@arsedizioni/ars-utils 22.6.85 → 22.6.88

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 (79) hide show
  1. package/fesm2022/arsedizioni-ars-utils-clipper.common.mjs +3158 -3344
  2. package/fesm2022/arsedizioni-ars-utils-clipper.common.mjs.map +1 -1
  3. package/fesm2022/arsedizioni-ars-utils-clipper.ui.mjs +7616 -4772
  4. package/fesm2022/arsedizioni-ars-utils-clipper.ui.mjs.map +1 -1
  5. package/fesm2022/arsedizioni-ars-utils-core.date.mjs +321 -702
  6. package/fesm2022/arsedizioni-ars-utils-core.date.mjs.map +1 -1
  7. package/fesm2022/arsedizioni-ars-utils-core.markdown.mjs +445 -590
  8. package/fesm2022/arsedizioni-ars-utils-core.markdown.mjs.map +1 -1
  9. package/fesm2022/arsedizioni-ars-utils-core.mjs +1735 -2370
  10. package/fesm2022/arsedizioni-ars-utils-core.mjs.map +1 -1
  11. package/fesm2022/arsedizioni-ars-utils-core.validators.mjs +1280 -1470
  12. package/fesm2022/arsedizioni-ars-utils-core.validators.mjs.map +1 -1
  13. package/fesm2022/arsedizioni-ars-utils-evolution.common.mjs +1097 -1528
  14. package/fesm2022/arsedizioni-ars-utils-evolution.common.mjs.map +1 -1
  15. package/fesm2022/arsedizioni-ars-utils-support.common.mjs +198 -294
  16. package/fesm2022/arsedizioni-ars-utils-support.common.mjs.map +1 -1
  17. package/fesm2022/arsedizioni-ars-utils-ui.controls.date.mjs +115 -117
  18. package/fesm2022/arsedizioni-ars-utils-ui.controls.date.mjs.map +1 -1
  19. package/fesm2022/arsedizioni-ars-utils-ui.controls.mjs +1607 -613
  20. package/fesm2022/arsedizioni-ars-utils-ui.controls.mjs.map +1 -1
  21. package/fesm2022/arsedizioni-ars-utils-ui.controls.tree.mjs +840 -367
  22. package/fesm2022/arsedizioni-ars-utils-ui.controls.tree.mjs.map +1 -1
  23. package/fesm2022/arsedizioni-ars-utils-ui.dialogs.auth.mjs +1634 -651
  24. package/fesm2022/arsedizioni-ars-utils-ui.dialogs.auth.mjs.map +1 -1
  25. package/fesm2022/arsedizioni-ars-utils-ui.dialogs.mjs +1255 -792
  26. package/fesm2022/arsedizioni-ars-utils-ui.dialogs.mjs.map +1 -1
  27. package/fesm2022/arsedizioni-ars-utils-ui.dialogs.prompt.mjs +1410 -357
  28. package/fesm2022/arsedizioni-ars-utils-ui.dialogs.prompt.mjs.map +1 -1
  29. package/fesm2022/arsedizioni-ars-utils-ui.dialogs.select.mjs +2401 -944
  30. package/fesm2022/arsedizioni-ars-utils-ui.dialogs.select.mjs.map +1 -1
  31. package/fesm2022/arsedizioni-ars-utils-ui.files.mjs +1553 -741
  32. package/fesm2022/arsedizioni-ars-utils-ui.files.mjs.map +1 -1
  33. package/fesm2022/arsedizioni-ars-utils-ui.filters.mjs +901 -430
  34. package/fesm2022/arsedizioni-ars-utils-ui.filters.mjs.map +1 -1
  35. package/fesm2022/arsedizioni-ars-utils-ui.help.mjs +737 -530
  36. package/fesm2022/arsedizioni-ars-utils-ui.help.mjs.map +1 -1
  37. package/fesm2022/arsedizioni-ars-utils-ui.mjs +6638 -1755
  38. package/fesm2022/arsedizioni-ars-utils-ui.mjs.map +1 -1
  39. package/fesm2022/arsedizioni-ars-utils-ui.navigation.mjs +1479 -1207
  40. package/fesm2022/arsedizioni-ars-utils-ui.navigation.mjs.map +1 -1
  41. package/fesm2022/arsedizioni-ars-utils-ui.notifications.mjs +1003 -270
  42. package/fesm2022/arsedizioni-ars-utils-ui.notifications.mjs.map +1 -1
  43. package/fesm2022/arsedizioni-ars-utils-ui.oauth.mjs +497 -278
  44. package/fesm2022/arsedizioni-ars-utils-ui.oauth.mjs.map +1 -1
  45. package/fesm2022/arsedizioni-ars-utils-ui.paginator.mjs +48 -52
  46. package/fesm2022/arsedizioni-ars-utils-ui.paginator.mjs.map +1 -1
  47. package/fesm2022/arsedizioni-ars-utils-ui.shell.mjs +464 -704
  48. package/fesm2022/arsedizioni-ars-utils-ui.shell.mjs.map +1 -1
  49. package/fesm2022/arsedizioni-ars-utils-ui.tinymce.mjs +724 -657
  50. package/fesm2022/arsedizioni-ars-utils-ui.tinymce.mjs.map +1 -1
  51. package/fesm2022/arsedizioni-ars-utils.mjs +3 -19
  52. package/fesm2022/arsedizioni-ars-utils.mjs.map +1 -1
  53. package/package.json +1 -1
  54. package/types/arsedizioni-ars-utils-clipper.common.d.ts +1151 -1163
  55. package/types/arsedizioni-ars-utils-clipper.ui.d.ts +1588 -1426
  56. package/types/arsedizioni-ars-utils-core.d.ts +1169 -1172
  57. package/types/arsedizioni-ars-utils-core.date.d.ts +204 -208
  58. package/types/arsedizioni-ars-utils-core.markdown.d.ts +128 -126
  59. package/types/arsedizioni-ars-utils-core.validators.d.ts +270 -236
  60. package/types/arsedizioni-ars-utils-evolution.common.d.ts +1054 -1067
  61. package/types/arsedizioni-ars-utils-support.common.d.ts +225 -234
  62. package/types/arsedizioni-ars-utils-ui.controls.d.ts +440 -278
  63. package/types/arsedizioni-ars-utils-ui.controls.date.d.ts +24 -28
  64. package/types/arsedizioni-ars-utils-ui.controls.tree.d.ts +172 -136
  65. package/types/arsedizioni-ars-utils-ui.d.ts +2035 -821
  66. package/types/arsedizioni-ars-utils-ui.dialogs.auth.d.ts +327 -322
  67. package/types/arsedizioni-ars-utils-ui.dialogs.d.ts +343 -347
  68. package/types/arsedizioni-ars-utils-ui.dialogs.prompt.d.ts +167 -167
  69. package/types/arsedizioni-ars-utils-ui.dialogs.select.d.ts +373 -367
  70. package/types/arsedizioni-ars-utils-ui.files.d.ts +335 -283
  71. package/types/arsedizioni-ars-utils-ui.filters.d.ts +232 -160
  72. package/types/arsedizioni-ars-utils-ui.help.d.ts +204 -211
  73. package/types/arsedizioni-ars-utils-ui.navigation.d.ts +811 -646
  74. package/types/arsedizioni-ars-utils-ui.notifications.d.ts +74 -78
  75. package/types/arsedizioni-ars-utils-ui.oauth.d.ts +102 -62
  76. package/types/arsedizioni-ars-utils-ui.paginator.d.ts +19 -22
  77. package/types/arsedizioni-ars-utils-ui.shell.d.ts +376 -382
  78. package/types/arsedizioni-ars-utils-ui.tinymce.d.ts +244 -231
  79. package/types/arsedizioni-ars-utils.d.ts +1 -3
@@ -1,2376 +1,1741 @@
1
- import * as i0 from '@angular/core';
2
- import { EventEmitter, signal, computed, inject, ElementRef, afterNextRender, Directive, input, DestroyRef, HostListener, output, Pipe, Service, PLATFORM_ID, RendererFactory2 } from '@angular/core';
3
- import { SelectionModel } from '@angular/cdk/collections';
4
- import { parseISO, parse, format } from 'date-fns';
5
- import { it } from 'date-fns/locale';
6
- import { TZDate } from '@date-fns/tz';
7
- import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
8
- import { Subject, filter as filter$1, map as map$1, BehaviorSubject } from 'rxjs';
9
- import { debounceTime, filter, map } from 'rxjs/operators';
10
- import { DomSanitizer } from '@angular/platform-browser';
11
- import { isPlatformBrowser } from '@angular/common';
12
-
13
- class FileInfo {
14
- isValid() {
15
- return this.valid;
16
- }
17
- }
18
- class ValueModel {
19
- }
20
- class IDModel {
21
- }
22
- class GroupModel {
23
- }
24
- class DeleteModel extends GroupModel {
25
- }
26
- class RelationModel {
27
- }
28
- class UpdateRelationsModel {
29
- }
30
- /** Identifies the external identity provider used for an OAuth login. */
1
+ import * as i0 from "@angular/core";
2
+ import { DestroyRef, Directive, ElementRef, EventEmitter, HostListener, PLATFORM_ID, Pipe, RendererFactory2, Service, afterNextRender, computed, inject, input, output, signal } from "@angular/core";
3
+ import { SelectionModel } from "@angular/cdk/collections";
4
+ import { format, parse, parseISO } from "date-fns";
5
+ import { it } from "date-fns/locale";
6
+ import { TZDate } from "@date-fns/tz";
7
+ import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
8
+ import { BehaviorSubject, Subject, filter, map } from "rxjs";
9
+ import { debounceTime, filter as filter$1, map as map$1 } from "rxjs/operators";
10
+ import { DomSanitizer } from "@angular/platform-browser";
11
+ import { isPlatformBrowser } from "@angular/common";
12
+ var FileInfo = class {
13
+ isValid() {
14
+ return this.valid;
15
+ }
16
+ };
17
+ var ValueModel = class {};
18
+ var IDModel = class {};
19
+ var GroupModel = class {};
20
+ var DeleteModel = class extends GroupModel {};
21
+ var RelationModel = class {};
22
+ var UpdateRelationsModel = class {};
31
23
  var LoginOAuthType;
32
- (function (LoginOAuthType) {
33
- LoginOAuthType[LoginOAuthType["None"] = 0] = "None";
34
- LoginOAuthType[LoginOAuthType["Microsoft"] = 1] = "Microsoft";
35
- LoginOAuthType[LoginOAuthType["Google"] = 2] = "Google";
24
+ (function(LoginOAuthType) {
25
+ LoginOAuthType[LoginOAuthType["None"] = 0] = "None";
26
+ LoginOAuthType[LoginOAuthType["Microsoft"] = 1] = "Microsoft";
27
+ LoginOAuthType[LoginOAuthType["Google"] = 2] = "Google";
36
28
  })(LoginOAuthType || (LoginOAuthType = {}));
37
- class QueryModel {
38
- }
39
- class ImportModel {
40
- }
41
- class DateInterval {
42
- get fromAsDate() {
43
- if (this.from) {
44
- if (!(this.from instanceof Date)) {
45
- this.from = new Date(this.from);
46
- }
47
- if (this.from) {
48
- return new Date(this.from.getFullYear(), this.from.getMonth(), this.from.getDate(), 2, 0, 0);
49
- }
50
- }
51
- return undefined;
52
- }
53
- get toAsDate() {
54
- if (this.to) {
55
- if (!(this.to instanceof Date)) {
56
- this.to = new Date(this.to);
57
- }
58
- if (this.to) {
59
- return new Date(this.to.getFullYear(), this.to.getMonth(), this.to.getDate(), 2, 0, 0);
60
- }
61
- }
62
- return undefined;
63
- }
64
- constructor(from, to) {
65
- this.from = from;
66
- this.to = to;
67
- }
68
- clear() {
69
- this.from = undefined;
70
- this.to = undefined;
71
- }
72
- }
73
-
74
- /**
75
- * Conversions between the objects the API speaks (DTOs) and the models a signal form works with.
76
- *
77
- * The two shapes differ in one systematic way. A DTO leaves out what it does not have: a place
78
- * with no phone number simply has no `phone`. A form model cannot afford that — signal forms only
79
- * build a child field for a key whose value is defined, and the string validators need a `string`
80
- * rather than `string | undefined` — so every editable field is present, holding its empty value.
81
- *
82
- * Doing that conversion by hand means a `?? ''` per field on the way in and a `|| undefined` per
83
- * field on the way out, twice per form, kept in sync forever. {@link DtoUtils.toForm} and
84
- * {@link DtoUtils.toPayload} do it once, driven by a single "empty model" constant that also serves as
85
- * the initial value of the form and as what a "clear" button restores.
86
- *
87
- * ## The convention
88
- *
89
- * 1. **A component that edits a DTO owns a form model of its own.** `ProfileEditComponent`
90
- * receiving a `ProfileModel` declares a `ProfileForm`, derived from it — never binds the DTO
91
- * to the form directly. The DTO stays free to say "absent"; the form is not.
92
- * 2. **What is optional in the DTO becomes `| null` in the form model.** `phone?: string` becomes
93
- * `phone: string` holding `''`, `level?: Level` becomes `level: Level | null`. The optionality
94
- * is not lost, it moves into the empty value, and {@link Dto} gives it back on the way out.
95
- * 3. **A date field is `Date | null`.** Always a `Date`, never a `Date | string`; `null` is its
96
- * empty value, because that is what Material's date accessors — the single datepicker and both
97
- * boxes of a `mat-date-range-picker` — write when an input is cleared.
98
- * 4. **The empty model is named `_EMPTY_ITEM`.** One constant per form, next to its interface.
99
- * 5. **The two conversions are called `toForm()` and `toPayload()`, and are written one after the
100
- * other.** When a component builds more than one payload, the others are `toPayloadXxx()` and
101
- * the main one keeps the bare name.
102
- * 6. **A payload always goes through `toPayload()` before it reaches the API.** When the payload
103
- * needs adjustments the generic conversion cannot make — a bitmask to rebuild, dates to
104
- * normalize, a number to coerce, fields the model does not own — the component declares its own
105
- * `toPayload()` and calls {@link DtoUtils.toPayload} inside it for the field-by-field part.
106
- *
107
- * @example
108
- * ```ts
109
- * interface ProfileForm {
110
- * name: string; // name?: string in ProfileModel
111
- * level: Level | null; // level?: Level
112
- * from: Date | null; // fromDate?: Date, bound to a mat-date-range-picker
113
- * to: Date | null; // toDate?: Date
114
- * active: boolean;
115
- * }
116
- *
117
- * const _EMPTY_ITEM: ProfileForm = { name: '', level: null, from: null, to: null, active: false };
118
- *
119
- * private toForm(dto?: ProfileModel): ProfileForm {
120
- * return DtoUtils.toForm(_EMPTY_ITEM, dto as Partial<ProfileForm>);
121
- * }
122
- *
123
- * private toPayload(): ProfileModel {
124
- * const { from, to, ...rest } = this.model();
125
- * return { ...DtoUtils.toPayload(rest), fromDate: from ?? undefined, toDate: to ?? undefined };
126
- * }
127
- * ```
128
- */
129
- /** The two conversions between an API DTO and the model a signal form works with. */
130
- class DtoUtils {
131
- /**
132
- * Builds a form model from the object returned by the API.
133
- *
134
- * The shape of the result is decided by `empty`, never by the DTO: every key of `empty` is in
135
- * the result, taken from the DTO when it carries a usable value and left at its empty value
136
- * otherwise. A DTO field that is absent and one that is explicitly `null` are treated the same,
137
- * because both mean "the record has no value here" and the form wants its empty value for both.
138
- *
139
- * Keys that exist only in the model (a multi-select decomposed from a bitmask, a checkbox
140
- * extracted from a flags value) simply keep their empty value: fill them in around the call.
141
- *
142
- * @param empty - The neutral model: it declares every field the form owns, and the value each
143
- * one takes when the DTO does not carry it.
144
- * @param dto - The object coming from the API, or `undefined` when creating a new record.
145
- * @returns A new model, never the same reference as `empty` nor as `dto`.
146
- */
147
- static toForm(empty, dto) {
148
- const model = { ...empty };
149
- if (!dto)
150
- return model;
151
- for (const key of Object.keys(empty)) {
152
- const value = dto[key];
153
- if (value !== undefined && value !== null) {
154
- model[key] = value;
155
- }
156
- }
157
- return model;
158
- }
159
- /**
160
- * Builds the payload for the API from the current form model.
161
- *
162
- * Every empty value — `''`, `null` or `undefined` — is dropped, so a field the user never filled
163
- * in travels as absent instead of reaching the backend as an empty string that would blank the
164
- * record. `0` and `false` are NOT empty: an amount of zero and a switch left off are data, and a
165
- * filter that wants an unticked box to mean "no filter" has to say so where it builds its query.
166
- *
167
- * The conversion is shallow: nested objects and arrays are passed through as they are, so a
168
- * composite field (a bitmask to rebuild, a nested block to normalize) is handled around the call
169
- * — usually by destructuring it out of the model first.
170
- *
171
- * The return type is {@link Dto}, the model without its empty values, so what the compiler sees
172
- * matches what travels: a `coachLevel: CoachLevel | null` field arrives as `CoachLevel` and fits
173
- * the optional `coachLevel?: CoachLevel` of the API model. At runtime the empty keys are simply
174
- * not there, which is what `HttpClient` serializes anyway.
175
- *
176
- * @param model - The current form model.
177
- * @returns A shallow copy without the empty fields.
178
- */
179
- static toPayload(model) {
180
- const dto = {};
181
- for (const [key, value] of Object.entries(model)) {
182
- if (value === '' || value === null || value === undefined)
183
- continue;
184
- dto[key] = value;
185
- }
186
- return dto;
187
- }
188
- }
189
-
190
- const UtilsMessages = {
191
- /**
192
- * Messages
193
- */
194
- // Select dialog
195
- UTILS_DIALOGS_SELECT_OPTIONS_CHANGED: '§utils-dialogs-select-options-changed'
29
+ var QueryModel = class {};
30
+ var ImportModel = class {};
31
+ var DateInterval = class {
32
+ get fromAsDate() {
33
+ if (this.from) {
34
+ if (!(this.from instanceof Date)) this.from = new Date(this.from);
35
+ if (this.from) return new Date(this.from.getFullYear(), this.from.getMonth(), this.from.getDate(), 2, 0, 0);
36
+ }
37
+ }
38
+ get toAsDate() {
39
+ if (this.to) {
40
+ if (!(this.to instanceof Date)) this.to = new Date(this.to);
41
+ if (this.to) return new Date(this.to.getFullYear(), this.to.getMonth(), this.to.getDate(), 2, 0, 0);
42
+ }
43
+ }
44
+ constructor(from, to) {
45
+ this.from = from;
46
+ this.to = to;
47
+ }
48
+ clear() {
49
+ this.from = void 0;
50
+ this.to = void 0;
51
+ }
196
52
  };
197
-
53
+ var DtoUtils = class {
54
+ static toForm(empty, dto) {
55
+ const model = { ...empty };
56
+ if (!dto) return model;
57
+ for (const key of Object.keys(empty)) {
58
+ const value = dto[key];
59
+ if (value !== void 0 && value !== null) model[key] = value;
60
+ }
61
+ return model;
62
+ }
63
+ static toPayload(model) {
64
+ const dto = {};
65
+ for (const [key, value] of Object.entries(model)) {
66
+ if (value === "" || value === null || value === void 0) continue;
67
+ dto[key] = value;
68
+ }
69
+ return dto;
70
+ }
71
+ };
72
+ const UtilsMessages = { UTILS_DIALOGS_SELECT_OPTIONS_CHANGED: "§utils-dialogs-select-options-changed" };
198
73
  var DateFormat;
199
- (function (DateFormat) {
200
- DateFormat[DateFormat["Short"] = 1] = "Short";
201
- DateFormat[DateFormat["Long"] = 2] = "Long";
202
- DateFormat[DateFormat["LongWithShortMonth"] = 3] = "LongWithShortMonth";
203
- DateFormat[DateFormat["LongWithWeekDay"] = 4] = "LongWithWeekDay";
204
- DateFormat[DateFormat["LongWithShortWeekDay"] = 5] = "LongWithShortWeekDay";
205
- DateFormat[DateFormat["MonthAndYear"] = 6] = "MonthAndYear";
206
- DateFormat[DateFormat["LongMonthAndYear"] = 7] = "LongMonthAndYear";
207
- DateFormat[DateFormat["WeekDay"] = 8] = "WeekDay";
208
- DateFormat[DateFormat["LongWeekDay"] = 9] = "LongWeekDay";
209
- DateFormat[DateFormat["DayAndMonth"] = 10] = "DayAndMonth";
210
- DateFormat[DateFormat["ShortUS"] = 11] = "ShortUS";
211
- DateFormat[DateFormat["ShortISO8601"] = 12] = "ShortISO8601"; // yyyy-mm-dd
74
+ (function(DateFormat) {
75
+ DateFormat[DateFormat["Short"] = 1] = "Short";
76
+ DateFormat[DateFormat["Long"] = 2] = "Long";
77
+ DateFormat[DateFormat["LongWithShortMonth"] = 3] = "LongWithShortMonth";
78
+ DateFormat[DateFormat["LongWithWeekDay"] = 4] = "LongWithWeekDay";
79
+ DateFormat[DateFormat["LongWithShortWeekDay"] = 5] = "LongWithShortWeekDay";
80
+ DateFormat[DateFormat["MonthAndYear"] = 6] = "MonthAndYear";
81
+ DateFormat[DateFormat["LongMonthAndYear"] = 7] = "LongMonthAndYear";
82
+ DateFormat[DateFormat["WeekDay"] = 8] = "WeekDay";
83
+ DateFormat[DateFormat["LongWeekDay"] = 9] = "LongWeekDay";
84
+ DateFormat[DateFormat["DayAndMonth"] = 10] = "DayAndMonth";
85
+ DateFormat[DateFormat["ShortUS"] = 11] = "ShortUS";
86
+ DateFormat[DateFormat["ShortISO8601"] = 12] = "ShortISO8601";
212
87
  })(DateFormat || (DateFormat = {}));
213
- class SystemUtils {
214
- /** Shared collator for locale-aware, case-insensitive string comparison. */
215
- static { this.collator = new Intl.Collator('it', { sensitivity: 'base', numeric: true }); }
216
- /**
217
- * Array find by key
218
- * @param array : the array to scan
219
- * @param key : key name
220
- * @param value : the value to search for
221
- * @returns : the property value or null
222
- */
223
- static arrayFindByKey(array, key, value) {
224
- if (!array)
225
- return undefined;
226
- for (const item of array) {
227
- if (item[key] === value) {
228
- return item;
229
- }
230
- }
231
- return undefined;
232
- }
233
- /**
234
- * Array find index by key
235
- * @param array : the array to scan
236
- * @param key : the key name
237
- * @param value : the value to search for
238
- * @returns : the array index or -1 if not found
239
- */
240
- static arrayFindIndexByKey(array, key, value) {
241
- if (!array)
242
- return -1;
243
- for (let i = 0; i < array.length; i++) {
244
- if (array[i][key] === value) {
245
- return i;
246
- }
247
- }
248
- return -1;
249
- }
250
- /**
251
- * Get a value from and array made of name|value items
252
- * @param array : the array to scan
253
- * @param value : the value to search for
254
- * @returns : the property value or null if not found
255
- */
256
- static arrayGetValue(array, value) {
257
- if (!array)
258
- return undefined;
259
- for (const item of array) {
260
- const i = item;
261
- if (i['value'] === value) {
262
- return i['name'] ?? i['id'];
263
- }
264
- }
265
- return undefined;
266
- }
267
- /**
268
- * Convert items to nodes into a tree structure
269
- * @param items : list of nodes
270
- * @param parent : parent node
271
- * @returns : an array of INode objects
272
- */
273
- static arrayToNodes(items, parent) {
274
- const nodes = [];
275
- items.forEach(n => {
276
- const node = {
277
- id: n.id.toString(),
278
- name: n.name,
279
- count: n.count ? n.count : 0,
280
- children: undefined,
281
- parent,
282
- bag: n,
283
- };
284
- nodes.push(node);
285
- node.children =
286
- n.children && n.children.length > 0
287
- ? this.arrayToNodes(n.children, node)
288
- : [];
289
- });
290
- return nodes;
291
- }
292
- /**
293
- * Comparator factory for sorting arrays of objects by a given property key.
294
- * Pass the element type for compile-time key checking:
295
- * `items.sort(SystemUtils.arraySortCompare<Employee>('lastName'))`.
296
- * @param key - Name of the property to sort by (autocompleted from `T` when provided).
297
- * @param order - Sort direction: `'asc'` (default) or `'desc'`.
298
- * @returns A comparator function that returns a negative, zero, or positive number.
299
- */
300
- static arraySortCompare(key, order = 'asc') {
301
- const dir = order === 'desc' ? -1 : 1;
302
- return (a, b) => {
303
- const varA = a[key];
304
- const varB = b[key];
305
- if (varA === varB)
306
- return 0;
307
- if (varA === undefined || varA === null)
308
- return -1 * dir;
309
- if (varB === undefined || varB === null)
310
- return 1 * dir;
311
- if (typeof varA === 'string' && typeof varB === 'string') {
312
- return SystemUtils.collator.compare(varA, varB) * dir;
313
- }
314
- return (varA > varB ? 1 : varA < varB ? -1 : 0) * dir;
315
- };
316
- }
317
- /**
318
- * Format weight
319
- * @param gr : grams
320
- * @returns : the formatted string
321
- */
322
- static formatWeight(gr) {
323
- if (gr > 1000000)
324
- return `${(gr / 1000000).toFixed(2)} t`;
325
- else if (gr > 100000)
326
- return `${(gr / 100000).toFixed(2)} q`;
327
- else if (gr > 1000)
328
- return `${(gr / 1000).toFixed(2)} kg`;
329
- else
330
- return `${gr} gr`;
331
- }
332
- /**
333
- * Format file size
334
- * @param bytes : number of bytes
335
- * @returns : the formatted string
336
- */
337
- static formatFileSize(bytes) {
338
- const MB = 1024 * 1024;
339
- if (bytes >= MB)
340
- return `${(bytes / MB).toFixed(1)} MB`;
341
- if (bytes >= 1024)
342
- return `${(bytes / 1024).toFixed(1)} KB`;
343
- return `${bytes} byte`;
344
- }
345
- /**
346
- * Compare two strings (case-insensitive, locale-aware, accent-insensitive).
347
- * @param a : string a
348
- * @param b : string b
349
- * @returns : 0 if equals, 1 if bigger, -1 if lower
350
- */
351
- static compareString(a, b) {
352
- return this.collator.compare(a ?? '', b ?? '');
353
- }
354
- /**
355
- * Capitalize a string
356
- * @param s : the string to capitalize
357
- * @returns : the capitalized string
358
- */
359
- static capitalize(s) {
360
- if (!s)
361
- return undefined;
362
- let b = "";
363
- let cap = true;
364
- for (const char of s) {
365
- if (char === " ") {
366
- b += char;
367
- cap = true;
368
- }
369
- else if (cap) {
370
- b += char.toUpperCase();
371
- cap = false;
372
- }
373
- else {
374
- b += char.toLowerCase();
375
- }
376
- }
377
- return b;
378
- }
379
- /**
380
- * Truncate a string at the last word boundary before `max`.
381
- * @param s : the string to truncate
382
- * @param max : the max number of chars
383
- * @returns : the truncated string
384
- */
385
- static truncate(s, max = 500) {
386
- if (!s)
387
- return undefined;
388
- if (s.length <= max)
389
- return s;
390
- const i = s.lastIndexOf(' ', max - 1);
391
- return i > 0 ? s.slice(0, i) : s;
392
- }
393
- /**
394
- * Join a list of strings
395
- * @param items : the list of strings
396
- * @param sep : the separator string
397
- * @param max : the maximum resulting string
398
- * @returns : the joined string
399
- */
400
- static join(items, sep = " ", max = 350) {
401
- if (!items || items.length === 0)
402
- return undefined;
403
- if (items.length > 1) {
404
- let l = 0;
405
- let s = "";
406
- while (s.length < max && items.length > l) {
407
- if (l > 0) {
408
- s += sep;
409
- }
410
- s += items[l++];
411
- }
412
- if (items.length > l) {
413
- s += "...";
414
- }
415
- return s;
416
- }
417
- return items[0];
418
- }
419
- /**
420
- * Normalize a string by converting it to lowercase and removing extra spaces, with special handling for acronyms and camelCase.
421
- * @param s : the string to normalize
422
- * @returns : The normalized string, or `undefined` if the input is falsy.
423
- */
424
- static normalizeDisplay(s) {
425
- if (!s)
426
- return s;
427
- return s
428
- .split(' ')
429
- .map((word, wordIndex) => {
430
- if (!word)
431
- return word;
432
- // If the word is all uppercase and contains at least one letter, we assume it's an acronym and leave it as is
433
- if (word === word.toUpperCase() && /[A-Z]/.test(word)) {
434
- return word;
435
- }
436
- // Otherwise, convert to lowercase letter by letter
437
- const chars = word.split('');
438
- return chars
439
- .map((char, charIndex) => {
440
- if (charIndex === 0 && wordIndex === 0) {
441
- // First letter of the first word:
442
- // remains uppercase only if the first 2 letters are both uppercase
443
- const secondChar = chars[1] ?? '';
444
- const keepUppercase = char === char.toUpperCase() &&
445
- secondChar === secondChar.toUpperCase() &&
446
- /[A-Z]/.test(char) &&
447
- /[A-Z]/.test(secondChar);
448
- return keepUppercase ? char : char.toLowerCase();
449
- }
450
- return char.toLowerCase();
451
- })
452
- .join('');
453
- })
454
- .join(' ');
455
- }
456
- /**
457
- * Wraps bare URLs in the given string with `<a>` anchor tags.
458
- * @param s - The plain-text or HTML string to process.
459
- * @returns The string with URLs replaced by clickable links, or `''` when `s` is falsy.
460
- */
461
- static replaceAsHtml(s) {
462
- if (!s)
463
- return '';
464
- return s.replace(/https?:\/\/[^\s<]+/gi, v => `<a href="${v}" target="_blank" rel="noopener">${v}</a>`);
465
- }
466
- /**
467
- * Compare two names
468
- * @param a : name a
469
- * @param b : name b
470
- * @returns : true if a equals b
471
- */
472
- static compareNames(a, b) {
473
- if (a)
474
- a = a.trim();
475
- if (b)
476
- b = b.trim();
477
- if (a && b && a.length !== b.length)
478
- return false;
479
- if (this.compareString(a, b) === 0)
480
- return true;
481
- const p1 = (a ?? '').split(' ');
482
- const p2 = (b ?? '').split(' ');
483
- if (p1.length !== p2.length)
484
- return false;
485
- let matches = p1.length;
486
- const used = new Array(p2.length).fill(false);
487
- for (const s1 of p1) {
488
- for (let j = 0; j < p2.length; j++) {
489
- if (!used[j] && this.compareString(s1, p2[j]) === 0) {
490
- used[j] = true;
491
- matches--;
492
- break;
493
- }
494
- }
495
- }
496
- return matches === 0;
497
- }
498
- /**
499
- * Obfuscate a text with a key-driven code point rotation.
500
- *
501
- * @deprecated This is OBFUSCATION, not cryptography: it hides values from
502
- * casual inspection but offers no real security. Use {@link encrypt} /
503
- * {@link decrypt} (AES-GCM via Web Crypto) for anything sensitive.
504
- *
505
- * The rotation operates on Unicode code points and skips the surrogate
506
- * range, so the output is always a well-formed string: emoji and other
507
- * astral characters round-trip correctly (the previous code-unit based
508
- * version could emit lone surrogates and corrupt them).
509
- *
510
- * BREAKING CHANGE: values encoded with the previous code-unit algorithm
511
- * cannot be decoded by this implementation.
512
- *
513
- * @param text : the text to encode or decode
514
- * @param key : the key
515
- * @param reverse : true to decode, false to encode
516
- * @returns : the obfuscated (or restored) text, or undefined when text/key are empty
517
- */
518
- static cipher(text, key, reverse = false) {
519
- if (!text || !key)
520
- return undefined;
521
- // Valid Unicode scalar values: [0, 0xD7FF] + [0xE000, 0x10FFFF].
522
- // We rotate inside this contiguous "logical" space (surrogates excluded)
523
- // so the result is always a valid code point.
524
- const SURROGATE_START = 0xd800;
525
- const SURROGATE_SIZE = 0x800;
526
- const SPACE = 0x110000 - SURROGATE_SIZE; // number of valid scalar values
527
- const toIndex = (cp) => (cp < SURROGATE_START ? cp : cp - SURROGATE_SIZE);
528
- const toCodePoint = (i) => (i < SURROGATE_START ? i : i + SURROGATE_SIZE);
529
- const keyLen = key.length;
530
- let result = '';
531
- let i = 0;
532
- for (const ch of text) { // iterates by code point, not code unit
533
- let rotation = key.charCodeAt(i % keyLen);
534
- if (reverse)
535
- rotation = -rotation;
536
- const index = toIndex(ch.codePointAt(0));
537
- const rotated = ((index + rotation) % SPACE + SPACE) % SPACE;
538
- result += String.fromCodePoint(toCodePoint(rotated));
539
- i++;
540
- }
541
- return result;
542
- }
543
- // #region Real encryption (Web Crypto, AES-GCM)
544
- /** Payload format version, first byte of the binary envelope. */
545
- static { this.CIPHER_VERSION = 1; }
546
- /** PBKDF2 salt length in bytes. */
547
- static { this.CIPHER_SALT_LENGTH = 16; }
548
- /** AES-GCM IV length in bytes (96 bit, the recommended size for GCM). */
549
- static { this.CIPHER_IV_LENGTH = 12; }
550
- /** PBKDF2-HMAC-SHA-256 iterations (OWASP recommendation). */
551
- static { this.CIPHER_ITERATIONS = 600000; }
552
- /**
553
- * Derives an AES-GCM 256-bit key from a password using PBKDF2-HMAC-SHA-256.
554
- * @param password : the user password / passphrase
555
- * @param salt : the random salt bound to this payload
556
- * @returns : a non-extractable AES-GCM CryptoKey
557
- */
558
- static async deriveKey(password, salt) {
559
- const material = await crypto.subtle.importKey('raw', new TextEncoder().encode(password), 'PBKDF2', false, ['deriveKey']);
560
- return crypto.subtle.deriveKey({ name: 'PBKDF2', salt: salt, iterations: this.CIPHER_ITERATIONS, hash: 'SHA-256' }, material, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt']);
561
- }
562
- /**
563
- * Encodes bytes as URL-safe Base64 (no '+', '/' or trailing '=').
564
- * @param bytes : the bytes to encode
565
- * @returns : the base64url string
566
- */
567
- static toBase64Url(bytes) {
568
- let binary = '';
569
- const CHUNK = 0x8000;
570
- for (let i = 0; i < bytes.length; i += CHUNK) {
571
- binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
572
- }
573
- return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
574
- }
575
- /**
576
- * Decodes a URL-safe Base64 string back to bytes.
577
- * @param value : the base64url string
578
- * @returns : the decoded bytes
579
- */
580
- static fromBase64Url(value) {
581
- const b64 = value.replace(/-/g, '+').replace(/_/g, '/');
582
- const padded = b64 + '='.repeat((4 - (b64.length % 4)) % 4);
583
- const binary = atob(padded);
584
- const bytes = new Uint8Array(binary.length);
585
- for (let i = 0; i < binary.length; i++)
586
- bytes[i] = binary.charCodeAt(i);
587
- return bytes;
588
- }
589
- /**
590
- * Encrypts a text with a password using AES-GCM (256 bit) and a key derived
591
- * via PBKDF2-HMAC-SHA-256 with a random per-message salt.
592
- *
593
- * The result is a self-contained, URL-safe Base64 payload:
594
- * `version (1 byte) | salt (16 bytes) | iv (12 bytes) | ciphertext+tag`.
595
- * GCM is authenticated: any tampering makes {@link decrypt} fail.
596
- *
597
- * Requires a secure context (HTTPS or localhost) for `crypto.subtle`.
598
- *
599
- * @param text : the plain text to encrypt
600
- * @param password : the password / passphrase
601
- * @returns : the base64url payload, or undefined when text/password are
602
- * empty or the Web Crypto API is unavailable
603
- */
604
- static async encrypt(text, password) {
605
- if (!text || !password)
606
- return undefined;
607
- if (typeof crypto === 'undefined' || !crypto.subtle)
608
- return undefined;
609
- try {
610
- const salt = crypto.getRandomValues(new Uint8Array(this.CIPHER_SALT_LENGTH));
611
- const iv = crypto.getRandomValues(new Uint8Array(this.CIPHER_IV_LENGTH));
612
- const key = await this.deriveKey(password, salt);
613
- const cipherText = new Uint8Array(await crypto.subtle.encrypt({ name: 'AES-GCM', iv: iv }, key, new TextEncoder().encode(text)));
614
- const payload = new Uint8Array(1 + salt.length + iv.length + cipherText.length);
615
- payload[0] = this.CIPHER_VERSION;
616
- payload.set(salt, 1);
617
- payload.set(iv, 1 + salt.length);
618
- payload.set(cipherText, 1 + salt.length + iv.length);
619
- return this.toBase64Url(payload);
620
- }
621
- catch {
622
- return undefined;
623
- }
624
- }
625
- /**
626
- * Decrypts a payload produced by {@link encrypt}.
627
- * Returns undefined when the password is wrong, the payload was tampered
628
- * with, the format is unknown, or the Web Crypto API is unavailable.
629
- *
630
- * @param payload : the base64url payload returned by {@link encrypt}
631
- * @param password : the password / passphrase used to encrypt
632
- * @returns : the original plain text, or undefined on any failure
633
- */
634
- static async decrypt(payload, password) {
635
- if (!payload || !password)
636
- return undefined;
637
- if (typeof crypto === 'undefined' || !crypto.subtle)
638
- return undefined;
639
- try {
640
- const bytes = this.fromBase64Url(payload);
641
- const headerLength = 1 + this.CIPHER_SALT_LENGTH + this.CIPHER_IV_LENGTH;
642
- if (bytes.length <= headerLength || bytes[0] !== this.CIPHER_VERSION)
643
- return undefined;
644
- const salt = bytes.subarray(1, 1 + this.CIPHER_SALT_LENGTH);
645
- const iv = bytes.subarray(1 + this.CIPHER_SALT_LENGTH, headerLength);
646
- const cipherText = bytes.subarray(headerLength);
647
- const key = await this.deriveKey(password, salt);
648
- const plain = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: iv }, key, cipherText);
649
- return new TextDecoder().decode(plain);
650
- }
651
- catch {
652
- // Wrong password or tampered payload: GCM authentication failed
653
- return undefined;
654
- }
655
- }
656
- // #endregion
657
- /**
658
- * Clone an object (deep copy).
659
- * Uses native structuredClone: handles Date, Map, Set, typed arrays and circular references.
660
- * @param obj : the object to clone
661
- * @returns : a new object
662
- */
663
- static clone(obj) {
664
- if (obj === null || obj === undefined)
665
- return obj;
666
- return structuredClone(obj);
667
- }
668
- /**
669
- * Creates a deep clone of an object.
670
- * @param obj - The source object to clone.
671
- * @param dest - Optional pre-allocated destination object to merge the clone into.
672
- * @returns A deep copy of `obj`.
673
- */
674
- static deepClone(obj, dest) {
675
- const cloned = structuredClone(obj);
676
- if (dest) {
677
- Object.assign(dest, cloned);
678
- return dest;
679
- }
680
- return cloned;
681
- }
682
- /**
683
- * Returns `true` when `value` is a syntactically valid UUID string.
684
- * @param value - The string to validate.
685
- */
686
- static parseUUID(value) {
687
- if (!value)
688
- return false;
689
- let s = value.trim();
690
- // Strip a matching wrapper pair, if present
691
- if ((s.startsWith('{') && s.endsWith('}')) || (s.startsWith('(') && s.endsWith(')'))) {
692
- s = s.slice(1, -1);
693
- }
694
- return /^[0-9a-fA-F]{8}-(?:[0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}$/.test(s);
695
- }
696
- /**
697
- * Returns `true` when `value` is a valid, non-empty (non-zero) UUID.
698
- * @param value - The string to validate.
699
- */
700
- static parseUUIDNotEmpty(value) {
701
- return this.parseUUID(value) && !/^[{(]?0{8}-(?:0{4}-){3}0{12}[)}]?$/.test((value ?? '').trim());
702
- }
703
- /**
704
- * Return an empty UUID
705
- * @returns : the empty UUID
706
- */
707
- static emptyUUID() { return "00000000-0000-0000-0000-000000000000"; }
708
- ;
709
- /**
710
- * Create a new UUID
711
- * @returns : the string UUID
712
- */
713
- static generateUUID() {
714
- if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
715
- return crypto.randomUUID();
716
- }
717
- // Fallback (non-secure contexts)
718
- return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
719
- const r = (Math.random() * 16) | 0;
720
- const v = c === 'x' ? r : (r & 0x3) | 0x8;
721
- return v.toString(16);
722
- });
723
- }
724
- /**
725
- * Reconstruct a standard UUID (with dashes) from a 32-char hex string without dashes.
726
- * @param value : 32-character hex string
727
- * @returns : the formatted UUID or the original string if it doesn't match the expected format
728
- */
729
- static inflateUUID(value) {
730
- const s = value.trim();
731
- if (s.length !== 32)
732
- return s;
733
- return `${s.slice(0, 8)}-${s.slice(8, 12)}-${s.slice(12, 16)}-${s.slice(16, 20)}-${s.slice(20)}`;
734
- }
735
- /** Precompiled validation patterns. */
736
- 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; }
737
- static { this.urlRegex = /^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,12})(:\d{1,5})?([/\w .%-]*)?(\?[^\s#]*)?(#\S*)?$/i; }
738
- /**
739
- * Parse a text and return true if it is a valid email (or empty)
740
- * @param value : email
741
- * @returns : true if the email is valid or empty
742
- */
743
- static parseEmail(value) {
744
- if (!value)
745
- return true;
746
- return this.emailRegex.test(value.trim().toLowerCase());
747
- }
748
- /**
749
- * Parse a text containing one or more email addresses separated by `;` or `,`
750
- * and return true if all of them are valid (or the text is empty).
751
- * Empty items caused by double or trailing separators are ignored.
752
- * @param value : the email list to parse (e.g. "a@b.it; c@d.it")
753
- * @returns : true if all emails are valid or the value is empty
754
- */
755
- static parseEmails(value) {
756
- if (!value || value.trim().length === 0)
757
- return true;
758
- const items = value
759
- .split(/[;,]/)
760
- .map(e => e.trim().toLowerCase())
761
- .filter(e => e.length > 0);
762
- if (items.length === 0)
763
- return false; // only separators, no addresses
764
- return items.every(e => this.emailRegex.test(e));
765
- }
766
- /**
767
- * Parse a text and return true if it is a valid url (or empty)
768
- * @param value : the url to parse
769
- * @returns : true if the url is valid or empty
770
- */
771
- static parseUrl(value) {
772
- if (!value)
773
- return true;
774
- return this.urlRegex.test(value.trim().toLowerCase());
775
- }
776
- /**
777
- * Get date parts from a string value
778
- * @param value : the string to parse
779
- * @returns : an array of numbers with year, month, day
780
- */
781
- static getDateParts(value) {
782
- if (!value)
783
- return undefined;
784
- let parts = [];
785
- if (value.indexOf("-") !== -1) {
786
- const p = value.split("-");
787
- if (p.length !== 3)
788
- return undefined;
789
- parts = [parseInt(p[0]), parseInt(p[1]) - 1, parseInt(p[2])];
790
- }
791
- else if (value.indexOf("/") !== -1) {
792
- const p = value.split("/");
793
- if (p.length !== 3)
794
- return undefined;
795
- parts = [parseInt(p[2]), parseInt(p[1]) - 1, parseInt(p[0])];
796
- }
797
- else {
798
- return undefined;
799
- }
800
- return parts;
801
- }
802
- /**
803
- * Checks whether a value is a valid Date instance.
804
- * Note: do not use `date.getTime()` as a truthiness test, it is 0 (falsy)
805
- * for the Unix epoch (1970-01-01T00:00:00Z) even though the date is valid.
806
- * @param d : the value to check
807
- * @returns : true when `d` is a Date representing a valid point in time
808
- */
809
- static isValidDate(d) {
810
- return d instanceof Date && !isNaN(d.getTime());
811
- }
812
- /**
813
- * Parse a date
814
- * @param value : the value to check
815
- * @param locale : the locale to use
816
- * @returns : a valid Date object or undefined
817
- */
818
- static parseDate(value, locale = it) {
819
- // No value at all
820
- if (!value)
821
- return undefined;
822
- // A Date value
823
- if (value instanceof Date)
824
- return this.isValidDate(value) ? value : undefined;
825
- // Parse known formats using date-fns
826
- let d = parseISO(value);
827
- if (this.isValidDate(d) && d.getFullYear() > 1750)
828
- return d;
829
- d = parse(value, 'dd/MM/yyyy', new Date(), { locale: locale });
830
- if (this.isValidDate(d) && d.getFullYear() > 1750)
831
- return d;
832
- d = parse(value, 'yyyy-MM-dd', new Date(), { locale: locale });
833
- if (this.isValidDate(d) && d.getFullYear() > 1750)
834
- return d;
835
- // Parse values manually
836
- const parts = this.getDateParts(value);
837
- if (!parts)
838
- return undefined;
839
- if (parts[0] < 100)
840
- parts[0] += 2000;
841
- if (isNaN(parts[0]) || parts[0] < 0 || parts[0] < 1750)
842
- return undefined;
843
- if (isNaN(parts[1]) || parts[1] < 0)
844
- return undefined;
845
- if (isNaN(parts[2]) || parts[2] < 0)
846
- return undefined;
847
- if (parts[1] > 11)
848
- return undefined;
849
- if (parts[1] === 1 && parts[2] > 29)
850
- return undefined;
851
- else if (parts[1] !== 1 && parts[2] > 31)
852
- return undefined;
853
- return new TZDate(parts[0], parts[1], parts[2], 12);
854
- }
855
- /**
856
- * Converts a Date, timestamp or ISO string into a Europe/Rome Date whose JSON serialisation
857
- * emits a naive local datetime string ("yyyy-MM-dd'T'HH:mm:ss"), with no timezone designator,
858
- * so the wall-clock value round-trips to the server unchanged. Use this instead of `new Date(...)`
859
- * when reviving dates received from the API, to stay consistent with the DateFnsAdapter (values
860
- * entered through the datepicker already behave this way).
861
- * @param value : the source Date, Unix timestamp (ms) or ISO string
862
- * @returns : a Europe/Rome Date serialising as naive local time, or undefined for empty/invalid input
863
- */
864
- static toLocalDate(value) {
865
- if (value == null || value === '')
866
- return undefined;
867
- const ms = value instanceof Date ? value.getTime()
868
- : typeof value === 'number' ? value
869
- : new Date(value).getTime();
870
- if (isNaN(ms))
871
- return undefined;
872
- const t = new TZDate(ms, 'Europe/Rome');
873
- Object.defineProperty(t, 'toJSON', {
874
- value: () => format(t, "yyyy-MM-dd'T'HH:mm:ss"),
875
- enumerable: false,
876
- configurable: true,
877
- writable: true,
878
- });
879
- return t;
880
- }
881
- /**
882
- * Format a date
883
- * @param value : the date or string to format
884
- * @param fmt : the DateFormat to use or the string pattern
885
- * @param locale : the locale to use (default is IT)
886
- * @returns : the formatted string
887
- */
888
- static formatDate(value, fmt = DateFormat.Short, locale = it) {
889
- // No value at all
890
- if (!value)
891
- return '';
892
- // A string
893
- if (typeof value === 'string' || value instanceof String)
894
- value = this.parseDate(value);
895
- // Not a date
896
- if (!this.isValidDate(value))
897
- return '';
898
- // Format
899
- switch (fmt) {
900
- case DateFormat.Short: return format(value, "dd/MM/yyyy", { locale: locale });
901
- case DateFormat.Long: return format(value, "d MMMM yyyy", { locale: locale });
902
- case DateFormat.LongWithShortMonth: return format(value, "d MMM yyyy", { locale: locale });
903
- case DateFormat.LongWithWeekDay: return format(value, "EEEE, d MMMM yyyy", { locale: locale });
904
- case DateFormat.LongWithShortWeekDay: return format(value, "EEE, d MMMM yyyy", { locale: locale });
905
- case DateFormat.MonthAndYear: return format(value, "MMM yyyy", { locale: locale });
906
- case DateFormat.LongMonthAndYear: return format(value, "MMMM yyyy", { locale: locale });
907
- case DateFormat.WeekDay: return format(value, "EEE, d", { locale: locale });
908
- case DateFormat.LongWeekDay: return format(value, "EEEE, d", { locale: locale });
909
- case DateFormat.DayAndMonth: return format(value, "d MMMM", { locale: locale });
910
- case DateFormat.ShortUS: return format(value, "MM/dd/yyyy", { locale: locale });
911
- case DateFormat.ShortISO8601: return format(value, "yyyy-MM-dd", { locale: locale });
912
- default:
913
- return format(value, fmt, { locale: locale });
914
- }
915
- }
916
- /**
917
- * Update a DateInterval object according to a string
918
- * @param value : string value
919
- * @param interval : DateInterval value to update
920
- * @param end : true if must be updated the first or the end value
921
- * @param copy : copy the same value (works only if not end element)
922
- */
923
- static changeDateInterval(value, interval, end = false, copy = false) {
924
- if (value) {
925
- let year = -1;
926
- if (value.length === 4 && (year = parseInt(value)) > 1750) {
927
- if (!end) {
928
- interval.from = new TZDate(new Date(year, 0, 1), 'Europe/Rome');
929
- interval.to = new TZDate(new Date(year, 11, 31), 'Europe/Rome');
930
- }
931
- else
932
- interval.to = new Date(year, 11, 31);
933
- }
934
- else {
935
- const parts = this.getDateParts(value);
936
- if (!parts || parts.length !== 3)
937
- return;
938
- if (isNaN(parts[0]) || parts[0] < 1750)
939
- return;
940
- if (isNaN(parts[1]) || parts[1] < 0 || parts[1] > 11)
941
- return;
942
- if (isNaN(parts[2]) || parts[2] < 1 || parts[2] > 31)
943
- return;
944
- const d = new TZDate(new Date(parts[0], parts[1], parts[2]), 'Europe/Rome');
945
- if (end)
946
- interval.to = d;
947
- else if (copy) {
948
- interval.from = d;
949
- interval.to = d;
950
- }
951
- else
952
- interval.from = d;
953
- }
954
- }
955
- }
956
- /** Cache of `Intl.NumberFormat` instances (their construction is expensive). */
957
- static { this.numberFormatCache = new Map(); }
958
- /**
959
- * Returns a cached `Intl.NumberFormat` for the given options, creating it on first use.
960
- * @param locale - BCP 47 locale tag.
961
- * @param options - The `Intl.NumberFormat` options.
962
- * @returns A shared formatter instance.
963
- */
964
- static getNumberFormat(locale, options) {
965
- const key = `${locale}|${options.style}|${options.currency ?? ''}|${options.minimumFractionDigits}|${options.maximumFractionDigits}`;
966
- let fmt = this.numberFormatCache.get(key);
967
- if (!fmt) {
968
- fmt = new Intl.NumberFormat(locale, options);
969
- this.numberFormatCache.set(key, fmt);
970
- }
971
- return fmt;
972
- }
973
- /**
974
- * Formats a number using a cached `Intl.NumberFormat`.
975
- * @param value - The number to format.
976
- * @param decimals - Maximum decimal places (default: `2`).
977
- * @param locale - BCP 47 locale tag (default: `'it-IT'`).
978
- * @returns The formatted number string.
979
- */
980
- static formatNumber(value, decimals = 2, locale = 'it-IT') {
981
- return this.getNumberFormat(locale, { style: 'decimal', minimumFractionDigits: 0, maximumFractionDigits: decimals }).format(value);
982
- }
983
- /**
984
- * Formats a number as a currency string using a cached `Intl.NumberFormat`.
985
- * @param value - The number to format.
986
- * @param currency - ISO 4217 currency code (default: `'EUR'`).
987
- * @param decimals - Maximum decimal places (default: `2`).
988
- * @param locale - BCP 47 locale tag (default: `'it-IT'`).
989
- * @returns The formatted currency string.
990
- */
991
- static formatCurrency(value, currency = 'EUR', decimals = 2, locale = 'it-IT') {
992
- return this.getNumberFormat(locale, { style: 'currency', currency, minimumFractionDigits: 0, maximumFractionDigits: decimals }).format(value);
993
- }
994
- /**
995
- * Percent-encodes a string for safe inclusion in a URL.
996
- * @param value - The string to encode.
997
- * @returns The encoded string, or `undefined` when `value` is empty.
998
- */
999
- static urlEncode(value) {
1000
- return value.length > 0
1001
- ? encodeURIComponent(value)
1002
- : undefined;
1003
- }
1004
- /**
1005
- * Decodes a percent-encoded URL string, treating `+` as a space.
1006
- * @param value - The encoded string to decode.
1007
- * @returns The decoded string, or `undefined` when `value` is empty or absent.
1008
- */
1009
- static urlDecode(value) {
1010
- return value && value.length > 0
1011
- ? decodeURIComponent(value.replace(/\+/g, '%20'))
1012
- : undefined;
1013
- }
1014
- /**
1015
- * Reads a query string parameter value from the current page URL.
1016
- * @param name - The parameter name to look up.
1017
- * @returns The decoded parameter value, or `undefined` when absent or running server-side.
1018
- */
1019
- static getQueryStringValueByName(name) {
1020
- if (!this.isBrowser())
1021
- return undefined;
1022
- const v = new URLSearchParams(window.location.search).get(name);
1023
- return v && v !== 'null' ? v : undefined;
1024
- }
1025
- /**
1026
- * Generate a password
1027
- * @returns : the password string
1028
- */
1029
- static generatePassword(length = 12) {
1030
- const lower = 'abcdefghijkmnopqrstuvwxyz'; // no 'l' (ambiguous)
1031
- const upper = 'ABCDEFGHJKLMNPQRSTUVWXYZ'; // no 'I'/'O' (ambiguous)
1032
- const digits = '23456789'; // no '0'/'1' (ambiguous)
1033
- const symbols = '$!#%&*+-';
1034
- const all = lower + upper + digits + symbols;
1035
- // Secure random indices (crypto is available in browsers and Node)
1036
- const rnd = (max) => {
1037
- if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {
1038
- const buf = new Uint32Array(1);
1039
- crypto.getRandomValues(buf);
1040
- return buf[0] % max;
1041
- }
1042
- return Math.floor(Math.random() * max);
1043
- };
1044
- // Guarantee at least one char per class, fill the rest, then shuffle
1045
- const chars = [
1046
- lower[rnd(lower.length)],
1047
- upper[rnd(upper.length)],
1048
- digits[rnd(digits.length)],
1049
- symbols[rnd(symbols.length)],
1050
- ];
1051
- while (chars.length < Math.max(length, 8)) {
1052
- chars.push(all[rnd(all.length)]);
1053
- }
1054
- for (let i = chars.length - 1; i > 0; i--) {
1055
- const j = rnd(i + 1);
1056
- [chars[i], chars[j]] = [chars[j], chars[i]];
1057
- }
1058
- return chars.join('');
1059
- }
1060
- /**
1061
- * Calculate password strength
1062
- * @param password: the password to evaluate
1063
- * @returns the password strength info
1064
- */
1065
- static calculatePasswordStrength(password) {
1066
- if (password && password.length > 0) {
1067
- let score = 0;
1068
- const suggestions = [];
1069
- // Length
1070
- if (password.length >= 10)
1071
- score++;
1072
- else
1073
- suggestions.push('Usa almeno 10 caratteri.');
1074
- if (password.length >= 12)
1075
- score++;
1076
- else if (password.length >= 10)
1077
- suggestions.push('Considera di usare più di 12 caratteri.');
1078
- // Lowercase letters
1079
- if (/[a-z]/.test(password))
1080
- score++;
1081
- else
1082
- suggestions.push('Aggiungi lettere minuscole.');
1083
- // Uppercase letters
1084
- if (/[A-Z]/.test(password))
1085
- score++;
1086
- else
1087
- suggestions.push('Aggiungi lettere maiuscole.');
1088
- // Numbers
1089
- if (/\d/.test(password))
1090
- score++;
1091
- else
1092
- suggestions.push('Aggiungi numeri.');
1093
- // Special characters
1094
- if (/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password))
1095
- score++;
1096
- else
1097
- suggestions.push('Aggiungi caratteri speciali (!@#$%^&*).');
1098
- // Common patterns
1099
- if (/(.)\1{2,}/.test(password)) {
1100
- score = Math.max(0, score - 1);
1101
- suggestions.push('Evita di ripetere lo stesso carattere.');
1102
- }
1103
- if (/123|abc|qwe/i.test(password)) {
1104
- score = Math.max(0, score - 1);
1105
- suggestions.push('Evita sequenze comuni (123, abc, qwe).');
1106
- }
1107
- // Label and color
1108
- let label;
1109
- let color;
1110
- let isValid;
1111
- if (score <= 2) {
1112
- label = 'Molto debole';
1113
- color = '#f44336';
1114
- isValid = false;
1115
- }
1116
- else if (score <= 3) {
1117
- label = 'Debole';
1118
- color = '#ff9800';
1119
- isValid = false;
1120
- }
1121
- else if (score <= 4) {
1122
- label = 'Media';
1123
- color = '#ffc107';
1124
- isValid = true;
1125
- }
1126
- else if (score <= 5) {
1127
- label = 'Forte';
1128
- color = '#8bc34a';
1129
- isValid = true;
1130
- }
1131
- else {
1132
- label = 'Molto forte';
1133
- color = '#4caf50';
1134
- isValid = true;
1135
- }
1136
- return { score, label, color, suggestions, isValid };
1137
- }
1138
- else
1139
- return {
1140
- score: 0,
1141
- isValid: false,
1142
- suggestions: []
1143
- };
1144
- }
1145
- /**
1146
- * Check if current browser supports touch
1147
- * @returns : true if the display is touchable
1148
- */
1149
- static isTouchable() {
1150
- return (this.isBrowser() && ("ontouchstart" in window ||
1151
- navigator.maxTouchPoints > 0));
1152
- }
1153
- /**
1154
- * This check will prevent 'window' logic to be executed
1155
- * while executing the server rendering
1156
- * @returns : true if using the browser
1157
- */
1158
- static isBrowser() {
1159
- return typeof (window) !== 'undefined';
1160
- }
1161
- /**
1162
- * Convert folders in a tree of Node object.
1163
- * @param folders : the subfolders group or null to root
1164
- * @returns : a node list
1165
- */
1166
- static toNodes(folders) {
1167
- return this._toNodes(folders, undefined);
1168
- }
1169
- /**
1170
- * Convert folders in a tree of Node object.
1171
- * @param folders : the children group or null to root
1172
- * @param parent : the parent node
1173
- * @returns : a node list
1174
- */
1175
- static _toNodes(folders, parent) {
1176
- const nodes = [];
1177
- folders.forEach((n) => {
1178
- const node = {
1179
- id: n.id,
1180
- name: n.name,
1181
- count: n.count,
1182
- parent: parent,
1183
- children: undefined,
1184
- bag: n,
1185
- };
1186
- const children = n.children ?? n.subFolders ?? [];
1187
- if (children.length > 0) {
1188
- node.children = this._toNodes(children, node);
1189
- }
1190
- nodes.push(node);
1191
- });
1192
- return nodes;
1193
- }
1194
- /**
1195
- * Returns an array of individual power-of-2 flag values that are set in `value`.
1196
- * @param value - The bitmask to decompose.
1197
- * @param max - Upper-bound exponent: checks flags from `1` up to `1 << max` (default: `30`).
1198
- * @returns Array of set flag values, or an empty array when `value` is `0`.
1199
- */
1200
- static getFlags(value, max = 30) {
1201
- if (value !== 0) {
1202
- const items = [];
1203
- const top = Math.min(max, 30); // beyond 30 the shift overflows into the sign bit
1204
- for (let exp = 0; exp <= top; exp++) {
1205
- const v = 1 << exp;
1206
- if ((value & v) === v)
1207
- items.push(v);
1208
- }
1209
- return items;
1210
- }
1211
- else
1212
- return [];
1213
- }
1214
- /** Cache for resolved color luminance results. */
1215
- static { this.colorLightCache = new Map(); }
1216
- /**
1217
- * Check if a color is light or dark
1218
- * @param color : the color
1219
- * @param minimumLuminance : the lumimance to consider
1220
- * @returns true if the color is light
1221
- */
1222
- static isColorLight(color, minimumLuminance = 186) {
1223
- if (!this.isBrowser())
1224
- return true; // SSR fallback
1225
- const cacheKey = `${color}|${minimumLuminance}`;
1226
- const cached = this.colorLightCache.get(cacheKey);
1227
- if (cached !== undefined)
1228
- return cached;
1229
- const tempDiv = document.createElement('div');
1230
- tempDiv.style.color = color;
1231
- document.body.appendChild(tempDiv);
1232
- const rgb = getComputedStyle(tempDiv).color;
1233
- document.body.removeChild(tempDiv);
1234
- const result = rgb.match(/\d+/g);
1235
- let light = true; // fallback
1236
- if (result && result.length >= 3) {
1237
- const r = parseInt(result[0], 10);
1238
- const g = parseInt(result[1], 10);
1239
- const b = parseInt(result[2], 10);
1240
- light = (0.299 * r + 0.587 * g + 0.114 * b) > minimumLuminance;
1241
- }
1242
- this.colorLightCache.set(cacheKey, light);
1243
- return light;
1244
- }
1245
- }
1246
-
1247
- /**
1248
- * Generic selection model that tracks a set of selected items identified by a lookup field.
1249
- * Wraps Angular CDK's `SelectionModel` and adds lookup-based add/remove logic.
1250
- *
1251
- * @typeParam T - The item type held in the selection.
1252
- * @typeParam V - The type of the lookup key used to identify items.
1253
- */
1254
- class SelectableModel {
1255
- /**
1256
- * Snapshot of all items currently tracked by this model (selected or previously toggled).
1257
- * Backed by a signal — reads are always up to date.
1258
- */
1259
- get all() {
1260
- return this._all();
1261
- }
1262
- /** The underlying CDK `SelectionModel` (provides `.selected`, `.isSelected`, etc.). */
1263
- get current() {
1264
- return this._current;
1265
- }
1266
- /**
1267
- * @param allowMultiSelect - When `true` (default), multiple items can be selected simultaneously.
1268
- * @param lookupFieldName - Name of the field used as the unique key when searching the internal list
1269
- * (default: `'id'`). Keys of `T` are autocompleted; plain strings remain accepted.
1270
- */
1271
- constructor(allowMultiSelect = true, lookupFieldName = 'id') {
1272
- /**
1273
- * Emits whenever the selection changes.
1274
- * Carries the affected item, or `undefined` when the whole selection is cleared.
1275
- *
1276
- * Note: kept as `EventEmitter` (instead of a plain RxJS `Subject`) for
1277
- * backward compatibility with existing consumers; `EventEmitter` extends
1278
- * `Subject`, so `.subscribe()`, `pipe(...)` and `takeUntilDestroyed` all
1279
- * work as expected.
1280
- */
1281
- this.changed = new EventEmitter();
1282
- this._all = signal([], /* @ts-ignore */
1283
- ...(ngDevMode ? [{ debugName: "_all" }] : /* istanbul ignore next */ []));
1284
- /** Signal that is `true` when at least one item is tracked in the internal list. */
1285
- this.hasValue = computed(() => this._all().length > 0, /* @ts-ignore */
1286
- ...(ngDevMode ? [{ debugName: "hasValue" }] : /* istanbul ignore next */ []));
1287
- /** Signal with the number of items currently tracked in the internal list. */
1288
- this.count = computed(() => this._all().length, /* @ts-ignore */
1289
- ...(ngDevMode ? [{ debugName: "count" }] : /* istanbul ignore next */ []));
1290
- /** Signal that is `true` when exactly one item is currently selected. */
1291
- this.selectedOne = computed(() => this._all().length === 1, /* @ts-ignore */
1292
- ...(ngDevMode ? [{ debugName: "selectedOne" }] : /* istanbul ignore next */ []));
1293
- /** Signal that is `true` when two or more items are currently selected. */
1294
- this.selectedMany = computed(() => this._all().length > 1, /* @ts-ignore */
1295
- ...(ngDevMode ? [{ debugName: "selectedMany" }] : /* istanbul ignore next */ []));
1296
- /** Signal that is `true` when at least one element is selected. */
1297
- this.selectedAny = computed(() => this._all().length > 0, /* @ts-ignore */
1298
- ...(ngDevMode ? [{ debugName: "selectedAny" }] : /* istanbul ignore next */ []));
1299
- /** Signal that is `true` when no item is currently selected. */
1300
- this.selectedNone = computed(() => this._all().length === 0, /* @ts-ignore */
1301
- ...(ngDevMode ? [{ debugName: "selectedNone" }] : /* istanbul ignore next */ []));
1302
- /** Signal with the first currently selected item, or `undefined` when none is selected. */
1303
- this.first = computed(() => this._all()[0], /* @ts-ignore */
1304
- ...(ngDevMode ? [{ debugName: "first" }] : /* istanbul ignore next */ []));
1305
- this._current = new SelectionModel(allowMultiSelect, [], true,
1306
- // Compare by lookup key so different instances of the same logical item
1307
- // (e.g. after a data reload) are treated as the same selection entry.
1308
- (a, b) => a[lookupFieldName] === b[lookupFieldName]);
1309
- this._lookupFieldName = lookupFieldName;
1310
- }
1311
- /**
1312
- * Toggles the CDK selection state of an item that already exists in the tracked list.
1313
- * Has no effect when `lookupValue` does not match any tracked item.
1314
- * @param item - The item whose selection state should be toggled.
1315
- * @param lookupValue - The key value used to locate the item in the internal list.
1316
- */
1317
- updateCurrent(item, lookupValue) {
1318
- const p = SystemUtils.arrayFindIndexByKey(this._all(), this._lookupFieldName, lookupValue);
1319
- if (p !== -1) {
1320
- this._current.toggle(item);
1321
- this.changed.emit(item);
1322
- }
1323
- }
1324
- /**
1325
- * Toggles an item in both the internal list and the CDK selection.
1326
- * Adds the item when it is not yet tracked; removes it when it is.
1327
- * @param item - The item to toggle.
1328
- * @param lookupValue - The key value used to locate or register the item.
1329
- */
1330
- toggle(item, lookupValue) {
1331
- if (lookupValue === undefined)
1332
- return;
1333
- const p = SystemUtils.arrayFindIndexByKey(this._all(), this._lookupFieldName, lookupValue);
1334
- if (p === -1) {
1335
- this._all.update(arr => [...arr, item]);
1336
- }
1337
- else {
1338
- this._all.update(arr => arr.filter((_, i) => i !== p));
1339
- }
1340
- this._current.toggle(item);
1341
- this.changed.emit(item);
1342
- }
1343
- /**
1344
- * Adds an item to the internal list (when not already present) and marks it as selected.
1345
- * @param item - The item to select.
1346
- * @param lookupValue - The key value used to locate or register the item.
1347
- */
1348
- select(item, lookupValue) {
1349
- if (lookupValue === undefined)
1350
- return;
1351
- const p = SystemUtils.arrayFindIndexByKey(this._all(), this._lookupFieldName, lookupValue);
1352
- if (p === -1) {
1353
- this._all.update(arr => [...arr, item]);
1354
- }
1355
- this._current.select(item);
1356
- this.changed.emit(item);
1357
- }
1358
- /**
1359
- * Removes an item from the internal list and deselects it in the CDK model.
1360
- * Has no effect when the item is not currently tracked.
1361
- * @param item - The item to deselect.
1362
- * @param lookupValue - The key value used to locate the item in the internal list.
1363
- */
1364
- deselect(item, lookupValue) {
1365
- if (lookupValue === undefined)
1366
- return;
1367
- const p = SystemUtils.arrayFindIndexByKey(this._all(), this._lookupFieldName, lookupValue);
1368
- if (p !== -1) {
1369
- this._all.update(arr => arr.filter((_, i) => i !== p));
1370
- this._current.deselect(item);
1371
- this.changed.emit(item);
1372
- }
1373
- }
1374
- /**
1375
- * Deselects all items whose lookup key is contained in `lookupValues`.
1376
- * @param lookupValues - Array of key values identifying the items to deselect.
1377
- */
1378
- deselectByValues(lookupValues) {
1379
- if (lookupValues.length === 0)
1380
- return;
1381
- for (const key of lookupValues) {
1382
- const p = SystemUtils.arrayFindIndexByKey(this._all(), this._lookupFieldName, key);
1383
- if (p !== -1) {
1384
- const item = this._all()[p];
1385
- this._all.update(arr => arr.filter((_, i) => i !== p));
1386
- this._current.deselect(item);
1387
- this.changed.emit(item);
1388
- }
1389
- }
1390
- }
1391
- /**
1392
- * Runs `clearFunc` against every currently selected item, then clears the CDK selection.
1393
- * The internal tracked list is **not** affected; only the CDK selection is cleared.
1394
- * @param clearFunc - Callback invoked for each currently selected item before clearing.
1395
- */
1396
- clearCurrent(clearFunc) {
1397
- for (const item of this._current.selected) {
1398
- clearFunc(item);
1399
- }
1400
- this._current.clear();
1401
- this.changed.emit(undefined);
1402
- }
1403
- /**
1404
- * Clears both the internal tracked list and the CDK selection.
1405
- */
1406
- clear() {
1407
- this._all.set([]);
1408
- this._current.clear();
1409
- this.changed.emit(undefined);
1410
- }
1411
- /**
1412
- * Returns `true` when the item identified by `lookupValue` is present in the tracked list.
1413
- * @param lookupValue - The key value to look up.
1414
- */
1415
- isSelected(lookupValue) {
1416
- return (lookupValue !== undefined &&
1417
- SystemUtils.arrayFindIndexByKey(this._all(), this._lookupFieldName, lookupValue) !== -1);
1418
- }
1419
- }
1420
-
1421
- /**
1422
- * Directive that moves browser focus to the host element after the first render cycle.
1423
- * Apply `autoFocus` to any focusable element to set focus automatically on initialisation.
1424
- */
1425
- class AutoFocusDirective {
1426
- constructor() {
1427
- this.elementRef = inject(ElementRef);
1428
- afterNextRender(() => {
1429
- this.elementRef.nativeElement?.focus();
1430
- });
1431
- }
1432
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: AutoFocusDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1433
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.7", type: AutoFocusDirective, isStandalone: true, selector: "[autoFocus]", ngImport: i0 }); }
1434
- }
1435
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: AutoFocusDirective, decorators: [{
1436
- type: Directive,
1437
- args: [{
1438
- selector: '[autoFocus]',
1439
- standalone: true
1440
- }]
1441
- }], ctorParameters: () => [] });
1442
-
1443
- /**
1444
- * Directive that listens to `keyup` events on a date input and debounces changes
1445
- * into a {@link DateInterval} model, converting shorthand strings (e.g. "d/m") to dates.
1446
- * Apply `[dateIntervalChange]="interval"` to the host `<input>` element.
1447
- */
1448
- class DateIntervalChangeDirective {
1449
- constructor() {
1450
- /** The date interval model to update when the input value changes. */
1451
- this.dateIntervalChange = input(new DateInterval(), /* @ts-ignore */
1452
- ...(ngDevMode ? [{ debugName: "dateIntervalChange" }] : /* istanbul ignore next */ []));
1453
- /** When `true`, the directive updates the interval's end date; otherwise the start date. */
1454
- this.end = input(false, /* @ts-ignore */
1455
- ...(ngDevMode ? [{ debugName: "end" }] : /* istanbul ignore next */ []));
1456
- this.subject = new Subject();
1457
- this.destroyRef = inject(DestroyRef);
1458
- this.subject
1459
- .pipe(debounceTime(100), takeUntilDestroyed(this.destroyRef))
1460
- .subscribe((e) => {
1461
- const value = e.target?.value;
1462
- if (value !== undefined) {
1463
- SystemUtils.changeDateInterval(value, this.dateIntervalChange(), this.end(), e.key === ' ');
1464
- }
1465
- });
1466
- }
1467
- /**
1468
- * Handles `keydown` events on the host element.
1469
- * The space key must be blocked HERE: by the time `keyup` fires the character
1470
- * has already been inserted into the input, so `preventDefault` on `keyup`
1471
- * cannot stop it.
1472
- * @param e - The keyboard event emitted by the host input.
1473
- */
1474
- onKeydown(e) {
1475
- if (e.key === ' ') {
1476
- e.preventDefault();
1477
- e.stopPropagation();
1478
- }
1479
- }
1480
- /**
1481
- * Handles `keyup` events on the host element and forwards them to the debounce pipeline.
1482
- * @param e - The keyboard event emitted by the host input.
1483
- */
1484
- onKeyup(e) {
1485
- this.subject.next(e);
1486
- }
1487
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: DateIntervalChangeDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1488
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.7", type: DateIntervalChangeDirective, isStandalone: true, selector: "[dateIntervalChange]", inputs: { dateIntervalChange: { classPropertyName: "dateIntervalChange", publicName: "dateIntervalChange", isSignal: true, isRequired: false, transformFunction: null }, end: { classPropertyName: "end", publicName: "end", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "keydown": "onKeydown($event)", "keyup": "onKeyup($event)" } }, ngImport: i0 }); }
1489
- }
1490
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: DateIntervalChangeDirective, decorators: [{
1491
- type: Directive,
1492
- args: [{
1493
- selector: '[dateIntervalChange]',
1494
- standalone: true,
1495
- }]
1496
- }], ctorParameters: () => [], propDecorators: { dateIntervalChange: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateIntervalChange", required: false }] }], end: [{ type: i0.Input, args: [{ isSignal: true, alias: "end", required: false }] }], onKeydown: [{
1497
- type: HostListener,
1498
- args: ['keydown', ['$event']]
1499
- }], onKeyup: [{
1500
- type: HostListener,
1501
- args: ['keyup', ['$event']]
1502
- }] } });
1503
-
1504
- /**
1505
- * Directive that copies a string payload to the clipboard when the host element is clicked.
1506
- * Bind `[copyClipboard]="text"` to provide the content to copy and listen to `(copied)` for confirmation.
1507
- */
1508
- class CopyClipboardDirective {
1509
- constructor() {
1510
- /** The text to copy to the clipboard. Bound via the `copyClipboard` attribute. */
1511
- this.payload = input(undefined, { ...(ngDevMode ? { debugName: "payload" } : /* istanbul ignore next */ {}), alias: 'copyClipboard' });
1512
- /** Emits the copied text after a successful copy operation. */
1513
- this.copied = output({ alias: 'copied' });
1514
- }
1515
- /**
1516
- * Handles click events on the host element and copies the payload to the clipboard.
1517
- * Emits `copied` with the copied text on success.
1518
- * @param e - The mouse click event.
1519
- */
1520
- onClick(e) {
1521
- e.preventDefault();
1522
- const payload = this.payload();
1523
- if (!payload)
1524
- return;
1525
- if (SystemUtils.isBrowser()) {
1526
- const listener = (clipEvent) => {
1527
- clipEvent.clipboardData?.setData('text/html', payload);
1528
- clipEvent.clipboardData?.setData('text/plain', payload);
1529
- clipEvent.preventDefault();
1530
- this.copied.emit(payload);
1531
- };
1532
- document.addEventListener('copy', listener, false);
1533
- document.execCommand('copy');
1534
- document.removeEventListener('copy', listener, false);
1535
- }
1536
- }
1537
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: CopyClipboardDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1538
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.7", 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 }); }
1539
- }
1540
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: CopyClipboardDirective, decorators: [{
1541
- type: Directive,
1542
- args: [{
1543
- selector: '[copyClipboard]',
1544
- standalone: true
1545
- }]
1546
- }], propDecorators: { payload: [{ type: i0.Input, args: [{ isSignal: true, alias: "copyClipboard", required: false }] }], copied: [{ type: i0.Output, args: ["copied"] }], onClick: [{
1547
- type: HostListener,
1548
- args: ['click', ['$event']]
1549
- }] } });
1550
-
1551
- /**
1552
- * Directive that removes focus from the host element after it is clicked,
1553
- * preventing the browser from keeping a visible focus ring post-interaction.
1554
- * Apply `removeFocus` to any focusable element (e.g. a button).
1555
- */
1556
- class RemoveFocusDirective {
1557
- constructor() {
1558
- this.elementRef = inject(ElementRef);
1559
- }
1560
- /**
1561
- * Handles click events on the host element and blurs it on the next event-loop tick
1562
- * so that the click action completes before focus is removed.
1563
- */
1564
- onClick() {
1565
- const el = this.elementRef.nativeElement;
1566
- if (el && typeof el.blur === 'function') {
1567
- setTimeout(() => el.blur(), 0);
1568
- }
1569
- }
1570
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: RemoveFocusDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1571
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.7", type: RemoveFocusDirective, isStandalone: true, selector: "[removeFocus]", host: { listeners: { "click": "onClick()" } }, ngImport: i0 }); }
1572
- }
1573
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: RemoveFocusDirective, decorators: [{
1574
- type: Directive,
1575
- args: [{
1576
- selector: '[removeFocus]',
1577
- standalone: true
1578
- }]
1579
- }], propDecorators: { onClick: [{
1580
- type: HostListener,
1581
- args: ['click']
1582
- }] } });
1583
-
1584
- /**
1585
- * Pipe that converts plain-text newlines (`\r\n`, `\r`, `\n`) to HTML `<br>` tags
1586
- * and marks the result as trusted HTML so Angular does not escape it.
1587
- *
1588
- * Usage: `{{ text | formatHtml }}`
1589
- */
1590
- class FormatHtmlPipe {
1591
- constructor() {
1592
- this.sanitizer = inject(DomSanitizer);
1593
- }
1594
- /**
1595
- * Transforms a plain-text string into sanitized HTML by replacing newline characters
1596
- * with `<br>` tags.
1597
- * @param value - The input string to transform. Treated as an empty string when `undefined`.
1598
- * @returns A `SafeHtml` value that can be rendered with `[innerHTML]`.
1599
- */
1600
- transform(value) {
1601
- const escaped = (value ?? '')
1602
- .replaceAll('&', '&amp;')
1603
- .replaceAll('<', '&lt;')
1604
- .replaceAll('>', '&gt;')
1605
- .replaceAll('"', '&quot;');
1606
- return this.sanitizer.bypassSecurityTrustHtml(escaped.replaceAll(/(?:\r\n|\r|\n)/g, '<br>'));
1607
- }
1608
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: FormatHtmlPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
1609
- static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.1.7", ngImport: i0, type: FormatHtmlPipe, isStandalone: true, name: "formatHtml" }); }
1610
- }
1611
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: FormatHtmlPipe, decorators: [{
1612
- type: Pipe,
1613
- args: [{
1614
- name: 'formatHtml',
1615
- standalone: true
1616
- }]
1617
- }] });
1618
-
1619
- /**
1620
- * General-purpose formatting pipe that converts a raw value to a locale-aware string
1621
- * based on the specified format type.
1622
- *
1623
- * Supported types: `'date'` / `'D'`, `'currency'` / `'C'`, `'number'` / `'N'`,
1624
- * `'number0'` / `'N0'`, `'percentage'` / `'P'`.
1625
- *
1626
- * Usage: `{{ value | format:'currency' }}`
1627
- */
1628
- class FormatPipe {
1629
- /** Shared formatters: building an `Intl.NumberFormat` per call is expensive. */
1630
- static { this.currencyFormat = new Intl.NumberFormat('it-IT', { style: 'currency', currency: 'EUR' }); }
1631
- static { this.numberFormat = new Intl.NumberFormat('it-IT', { style: 'decimal', minimumFractionDigits: 0, maximumFractionDigits: 2 }); }
1632
- static { this.number0Format = new Intl.NumberFormat('it-IT', { style: 'decimal', minimumFractionDigits: 0, maximumFractionDigits: 0 }); }
1633
- static { this.percentFormat = new Intl.NumberFormat('it-IT', { style: 'percent', minimumFractionDigits: 0, maximumFractionDigits: 0 }); }
1634
- /**
1635
- * Formats a value according to the specified type and optional pattern.
1636
- * Returns `undefined` when the value is `null` or `undefined`, or when the type is unrecognised.
1637
- * @param value - The raw value to format.
1638
- * @param type - The format type identifier (default: `'date'`).
1639
- * @param pattern - The date pattern used when `type` is `'date'` (default: `'dd/MM/yyyy'`).
1640
- * @returns A formatted string, or `undefined` when the value cannot be formatted.
1641
- */
1642
- transform(value, type = 'date', pattern = 'dd/MM/yyyy') {
1643
- if (value === undefined || value === null)
1644
- return undefined;
1645
- switch (type) {
1646
- case 'D':
1647
- case 'date': {
1648
- const d = SystemUtils.parseDate(value, it);
1649
- if (d)
1650
- return format(d, pattern, { locale: it });
1651
- break;
1652
- }
1653
- case 'C':
1654
- case 'currency':
1655
- return FormatPipe.currencyFormat.format(value);
1656
- case 'N':
1657
- case 'number':
1658
- return FormatPipe.numberFormat.format(value);
1659
- case 'N0':
1660
- case 'number0':
1661
- return FormatPipe.number0Format.format(value);
1662
- case 'P':
1663
- case 'percentage':
1664
- return FormatPipe.percentFormat.format(value);
1665
- }
1666
- return undefined;
1667
- }
1668
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: FormatPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
1669
- static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.1.7", ngImport: i0, type: FormatPipe, isStandalone: true, name: "format" }); }
1670
- }
1671
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: FormatPipe, decorators: [{
1672
- type: Pipe,
1673
- args: [{
1674
- name: 'format',
1675
- standalone: true
1676
- }]
1677
- }] });
1678
-
1679
- /**
1680
- * Pipe that applies a global regex replacement on a string and returns the result
1681
- * as sanitized HTML. When `regexValue` is `'\n'` and no `replaceValue` is given,
1682
- * newlines are replaced with `<br>` tags.
1683
- *
1684
- * Usage: `{{ text | replace:'\n':'' }}`
1685
- */
1686
- class ReplacePipe {
1687
- constructor() {
1688
- this.sanitizer = inject(DomSanitizer);
1689
- }
1690
- /**
1691
- * Replaces all occurrences of `regexValue` in `value` with `replaceValue`.
1692
- * Returns `undefined` when `value` is empty or `undefined`.
1693
- * @param value - The source string to process.
1694
- * @param regexValue - The regex pattern string to match (applied with the global flag).
1695
- * @param replaceValue - The replacement string. Defaults to `'<br>'` when `regexValue` is `'\n'` and this is falsy.
1696
- * @returns A `SafeHtml` value with all matches replaced, or `undefined` when the input is empty.
1697
- */
1698
- transform(value, regexValue, replaceValue) {
1699
- if (!value)
1700
- return undefined;
1701
- const replacement = (regexValue === '\n' && !replaceValue) ? '<br>' : (replaceValue ?? '');
1702
- return this.sanitizer.bypassSecurityTrustHtml(value.replace(new RegExp(regexValue, 'g'), replacement));
1703
- }
1704
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: ReplacePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
1705
- static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.1.7", ngImport: i0, type: ReplacePipe, isStandalone: true, name: "replace" }); }
1706
- }
1707
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: ReplacePipe, decorators: [{
1708
- type: Pipe,
1709
- args: [{
1710
- name: 'replace',
1711
- standalone: true
1712
- }]
1713
- }] });
1714
-
1715
- /**
1716
- * Pipe that marks an HTML string as trusted so Angular does not escape it when
1717
- * bound via `[innerHTML]`.
1718
- *
1719
- * Usage: `<div [innerHTML]="html | safeHtml"></div>`
1720
- */
1721
- class SafeHtmlPipe {
1722
- constructor() {
1723
- this.sanitizer = inject(DomSanitizer);
1724
- }
1725
- /**
1726
- * Bypasses Angular's HTML sanitization and returns a `SafeHtml` instance.
1727
- * @param value - The raw HTML string to trust. Treated as an empty string when `undefined`.
1728
- * @returns A `SafeHtml` value that can be bound to `[innerHTML]` without escaping.
1729
- */
1730
- transform(value) {
1731
- return this.sanitizer.bypassSecurityTrustHtml(value ?? '');
1732
- }
1733
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: SafeHtmlPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
1734
- static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.1.7", ngImport: i0, type: SafeHtmlPipe, isStandalone: true, name: "safeHtml" }); }
1735
- }
1736
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: SafeHtmlPipe, decorators: [{
1737
- type: Pipe,
1738
- args: [{
1739
- name: 'safeHtml',
1740
- standalone: true
1741
- }]
1742
- }] });
1743
-
1744
- /**
1745
- * Pipe that marks a URL string as a trusted resource URL so Angular does not block it
1746
- * when bound to attributes such as `[src]` or `[href]` on iframes, objects, or embeds.
1747
- *
1748
- * Usage: `<iframe [src]="url | safeUrl"></iframe>`
1749
- */
1750
- class SafeUrlPipe {
1751
- constructor() {
1752
- this.sanitizer = inject(DomSanitizer);
1753
- }
1754
- /**
1755
- * Bypasses Angular's resource-URL sanitization and returns a `SafeResourceUrl` instance.
1756
- * @param value - The URL string to trust. Treated as an empty string when `undefined`.
1757
- * @returns A `SafeResourceUrl` that can be bound to resource URL attributes without blocking.
1758
- */
1759
- transform(value) {
1760
- return this.sanitizer.bypassSecurityTrustResourceUrl(value ?? '');
1761
- }
1762
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: SafeUrlPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
1763
- static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.1.7", ngImport: i0, type: SafeUrlPipe, isStandalone: true, name: "safeUrl" }); }
1764
- }
1765
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: SafeUrlPipe, decorators: [{
1766
- type: Pipe,
1767
- args: [{
1768
- name: 'safeUrl',
1769
- standalone: true
1770
- }]
1771
- }] });
1772
-
1773
- /**
1774
- * Impure pipe that filters an array using a caller-provided predicate function.
1775
- * Because the pipe is impure it re-evaluates on every change-detection cycle,
1776
- * which is necessary when the predicate's captured state changes.
1777
- *
1778
- * Usage: `*ngFor="let item of items | callback:myFilter"`
1779
- */
1780
- class SearchCallbackPipe {
1781
- /**
1782
- * Filters `items` by applying `callback` to each element.
1783
- * Returns the original array unchanged when either argument is falsy.
1784
- * @param items - The source array to filter. May be `undefined`.
1785
- * @param callback - A predicate function that returns `true` for items to keep.
1786
- * @returns A new filtered array, the original array when no callback is provided,
1787
- * or `undefined` when `items` is `undefined`.
1788
- */
1789
- transform(items, callback) {
1790
- if (!items || !callback)
1791
- return items;
1792
- return items.filter(item => callback(item));
1793
- }
1794
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: SearchCallbackPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
1795
- static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.1.7", ngImport: i0, type: SearchCallbackPipe, isStandalone: true, name: "callback", pure: false }); }
1796
- }
1797
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: SearchCallbackPipe, decorators: [{
1798
- type: Pipe,
1799
- args: [{
1800
- name: 'callback',
1801
- pure: false,
1802
- standalone: true
1803
- }]
1804
- }] });
1805
-
1806
- /**
1807
- * Pure pipe that filters an array of searchable items against a text query.
1808
- * Note: being pure, it re-runs only when the array REFERENCE or the query
1809
- * changes; mutate-in-place updates of the array are not detected.
1810
- *
1811
- * Each item is matched either via its `searchBag.name` property (when present)
1812
- * or by converting the item itself to a lowercase string. The optional `metadata`
1813
- * argument is updated in-place with the total item count and the filtered count,
1814
- * making it usable in the template alongside `*ngFor`.
1815
- *
1816
- * Usage:
1817
- * ```html
1818
- * <div *ngFor="let item of items | search:filterText:meta">...</div>
1819
- * <div>Showing {{ meta.count }} of {{ meta.total }}</div>
1820
- * ```
1821
- */
1822
- class SearchFilterPipe {
1823
- /**
1824
- * Filters `items` by performing a case-insensitive substring match against `value`.
1825
- * When `items` or `value` is falsy the original array is returned unfiltered.
1826
- * @param items - The source array to filter. May be `undefined`.
1827
- * @param value - The search text to match against each item. May be `undefined`.
1828
- * @param metadata - Optional object that is updated with `total` and `count` after filtering.
1829
- * @returns The filtered array, the original array when no filter text is given,
1830
- * or `undefined` when `items` is `undefined`.
1831
- */
1832
- transform(items, value, metadata) {
1833
- metadata ??= { total: 0, count: 0 };
1834
- if (!items || !value) {
1835
- metadata.total = items?.length ?? 0;
1836
- metadata.count = metadata.total;
1837
- return items;
1838
- }
1839
- const query = value.toLowerCase();
1840
- const result = items.filter(item => {
1841
- if (!item)
1842
- return false;
1843
- const text = item.searchBag?.name ?? (typeof item === 'string' ? item : undefined);
1844
- return text?.toLowerCase().includes(query) ?? false;
1845
- });
1846
- metadata.total = items.length;
1847
- metadata.count = result.length;
1848
- return result;
1849
- }
1850
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: SearchFilterPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
1851
- static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.1.7", ngImport: i0, type: SearchFilterPipe, isStandalone: true, name: "search" }); }
1852
- }
1853
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: SearchFilterPipe, decorators: [{
1854
- type: Pipe,
1855
- args: [{
1856
- name: 'search',
1857
- standalone: true
1858
- }]
1859
- }] });
1860
-
1861
- /**
1862
- * Standard Broadcast Messages Manager
1863
- * https://developer.mozilla.org/en-US/docs/Web/API/BroadcastChannel
1864
- */
1865
- /**
1866
- * Wrapper around the native Web `BroadcastChannel` API that adds typed messaging,
1867
- * multiple named subscriptions, and a graceful disposal mechanism.
1868
- *
1869
- * @see https://developer.mozilla.org/en-US/docs/Web/API/BroadcastChannel
1870
- */
1871
- class BroadcastChannelManager {
1872
- /**
1873
- * The name of the underlying `BroadcastChannel`, or `undefined` when the API
1874
- * is not supported by the current browser.
1875
- */
1876
- get currentBus() {
1877
- return this.channel?.name;
1878
- }
1879
- /**
1880
- * Observable that emits EVERY message bag received on the channel,
1881
- * regardless of `messageId`. Completed by {@link dispose}.
1882
- * Prefer {@link observe} to listen for a single message type.
1883
- */
1884
- get messages() {
1885
- return this.subject.asObservable();
1886
- }
1887
- /**
1888
- * Returns a typed Observable that emits only the messages with the given `messageId`.
1889
- *
1890
- * Unlike {@link subscribe} (one callback per id, replaced on re-registration),
1891
- * this supports any number of concurrent subscribers and integrates with the
1892
- * RxJS lifecycle (`takeUntilDestroyed`, `async` pipe, ...). Completed by {@link dispose}.
1893
- *
1894
- * @param messageId - The message type identifier to listen for.
1895
- * @returns An Observable of the matching typed message bags.
1896
- */
1897
- observe(messageId) {
1898
- return this.subject.pipe(filter(bag => bag.messageId === messageId), map(bag => bag));
1899
- }
1900
- /**
1901
- * Opens a `BroadcastChannel` with the given name (when the API is available).
1902
- * @param bus - The channel name to open (default: `'ARS-CHANNEL'`).
1903
- */
1904
- constructor(bus = 'ARS-CHANNEL') {
1905
- this.subject = new Subject();
1906
- this.subscriptions = [];
1907
- if (typeof window !== 'undefined' && 'BroadcastChannel' in window) {
1908
- this.channel = new BroadcastChannel(bus);
1909
- this.channel.onmessageerror = (e) => {
1910
- console.error('[BroadcastChannelManager] Message receive error', e);
1911
- };
1912
- this.channel.onmessage = (e) => {
1913
- const bag = e.data;
1914
- if (!bag)
1915
- return;
1916
- this.subject.next(bag);
1917
- const info = this.subscriptions.find(m => m.messageId === bag.messageId);
1918
- info?.action(bag);
1919
- };
1920
- }
1921
- else {
1922
- console.error('[BroadcastChannelManager] BroadcastChannel API is not supported in this browser');
1923
- }
1924
- }
1925
- /**
1926
- * Closes the underlying channel and completes the internal subject after a short delay
1927
- * to allow any in-flight messages to be delivered.
1928
- */
1929
- dispose() {
1930
- setTimeout(() => {
1931
- this.subscriptions = [];
1932
- this.channel?.close();
1933
- this.channel = undefined;
1934
- this.subject.complete();
1935
- }, 1000);
1936
- }
1937
- /** @internal Implementation that handles all overloads. */
1938
- sendMessage(messageIdOrBag, dataOrDelay, delay) {
1939
- // Disambiguate overloads based on the first argument:
1940
- // - string => (messageId, data?, delay?)
1941
- // - object => (bag, delay?)
1942
- let bag;
1943
- let effectiveDelay;
1944
- if (typeof messageIdOrBag === 'string') {
1945
- bag = { messageId: messageIdOrBag, data: dataOrDelay };
1946
- effectiveDelay = delay ?? 0;
1947
- }
1948
- else {
1949
- bag = messageIdOrBag;
1950
- effectiveDelay = (typeof dataOrDelay === 'number' ? dataOrDelay : 0);
1951
- }
1952
- const post = () => this.channel?.postMessage(bag);
1953
- if (effectiveDelay > 0) {
1954
- setTimeout(post, effectiveDelay);
1955
- }
1956
- else {
1957
- post();
1958
- }
1959
- }
1960
- /**
1961
- * Registers a handler for messages with the given `messageId`.
1962
- * If a handler for the same `messageId` is already registered it is replaced.
1963
- * @param args - The subscription descriptor containing the message ID and handler.
1964
- */
1965
- subscribe(args) {
1966
- const i = this.subscriptions.findIndex(m => m.messageId === args.messageId);
1967
- if (i === -1) {
1968
- this.subscriptions.push(args);
1969
- }
1970
- else {
1971
- this.subscriptions[i].action = args.action;
1972
- }
1973
- }
1974
- /**
1975
- * Removes the subscription for the given message ID, if one exists.
1976
- * @param messageId - The message type identifier whose subscription should be removed.
1977
- */
1978
- unsubscribe(messageId) {
1979
- const i = this.subscriptions.findIndex(m => m.messageId === messageId);
1980
- if (i !== -1) {
1981
- this.subscriptions.splice(i, 1);
1982
- }
1983
- }
1984
- /**
1985
- * Removes all registered subscriptions.
1986
- */
1987
- unsubscribeAll() {
1988
- this.subscriptions = [];
1989
- }
1990
- }
1991
-
1992
- const CHANNEL_NAME = 'D2693418-B18F-41BB-BAE9-41BB5407957A-CHANNEL';
1993
- /**
1994
- * Application-level messaging service that combines two transports:
1995
- * - An in-process RxJS `Subject` for same-tab communication (`sendMessage` / `getMessage`).
1996
- * - A native `BroadcastChannel` for cross-tab communication (`sendChannelMessage` / `subscribeChannelMessage`).
1997
- */
1998
- class BroadcastService {
1999
- constructor() {
2000
- this.subject = new Subject();
2001
- this.channel = new BroadcastChannelManager(CHANNEL_NAME);
2002
- }
2003
- /**
2004
- * Creates a new standalone `BroadcastChannelManager` instance bound to the shared ARS channel.
2005
- * Useful when a caller needs direct channel access outside the service.
2006
- * @returns A new `BroadcastChannelManager` connected to the shared channel.
2007
- */
2008
- static createChannel() {
2009
- return new BroadcastChannelManager(CHANNEL_NAME);
2010
- }
2011
- ngOnDestroy() {
2012
- this.channel?.dispose();
2013
- this.subject.complete();
2014
- }
2015
- /**
2016
- * Publishes a message to the in-process subject, optionally after a delay.
2017
- * @param id - The message type identifier.
2018
- * @param data - Optional payload to attach to the message.
2019
- * @param delay - Milliseconds to wait before publishing (default: `0`).
2020
- */
2021
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
2022
- sendMessage(id, data, delay = 0) {
2023
- if (delay > 0) {
2024
- setTimeout(() => this.subject.next({ id, data }), delay);
2025
- }
2026
- else {
2027
- this.subject.next({ id, data });
2028
- }
2029
- }
2030
- /**
2031
- * Publishes a typed message over the native `BroadcastChannel` (cross-tab), optionally after a delay.
2032
- * @param id - The message type identifier.
2033
- * @param data - Optional typed payload to send.
2034
- * @param delay - Milliseconds to wait before sending (forwarded to the channel manager).
2035
- */
2036
- sendChannelMessage(id, data, delay) {
2037
- this.channel.sendMessage(id, data, delay);
2038
- }
2039
- /**
2040
- * Registers a handler that is invoked whenever a channel message with the given `id` is received.
2041
- * @param id - The message type identifier to listen for.
2042
- * @param action - Callback invoked with the full `BroadcastChannelMessageBag` when a matching message arrives.
2043
- */
2044
- subscribeChannelMessage(id, action) {
2045
- this.channel.subscribe({ messageId: id, action });
2046
- }
2047
- /**
2048
- * Removes the channel subscription previously registered for the given `id`, if any.
2049
- * @param id - The message type identifier whose subscription should be removed.
2050
- */
2051
- unsubscribeChannelMessage(id) {
2052
- this.channel.unsubscribe(id);
2053
- }
2054
- /**
2055
- * Returns a typed Observable of the cross-tab channel messages with the given `id`.
2056
- * Supports multiple concurrent subscribers and the RxJS lifecycle operators,
2057
- * unlike `subscribeChannelMessage` (single callback per id).
2058
- * @param id - The message type identifier to listen for.
2059
- * @returns An Observable of the matching typed message bags.
2060
- */
2061
- observeChannelMessage(id) {
2062
- return this.channel.observe(id);
2063
- }
2064
- /**
2065
- * Returns an `Observable` that emits every in-process message published via `sendMessage`.
2066
- * @returns An observable stream of `BroadcastMessageInfo` objects.
2067
- */
2068
- getMessage() {
2069
- return this.subject.asObservable();
2070
- }
2071
- /**
2072
- * Returns a typed `Observable` of the in-process messages with the given `id`.
2073
- * @param id - The message type identifier to listen for.
2074
- * @returns An Observable emitting the typed `data` payload of each matching message.
2075
- */
2076
- observeMessage(id) {
2077
- return this.subject.pipe(filter$1(info => info.id === id), map$1(info => info.data));
2078
- }
2079
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: BroadcastService, deps: [], target: i0.ɵɵFactoryTarget.Service }); }
2080
- static { this.ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.1.7", ngImport: i0, type: BroadcastService }); }
2081
- }
2082
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: BroadcastService, decorators: [{
2083
- type: Service
2084
- }] });
2085
-
2086
- /**
2087
- * Application-wide environment configuration service.
2088
- * Exposes writable signals for the base URIs used throughout the application.
2089
- * Values should be set once during application bootstrap (e.g. in `APP_INITIALIZER`).
2090
- */
2091
- class EnvironmentService {
2092
- constructor() {
2093
- /** Writable signal holding the base URI of the frontend application (e.g. `https://app.example.com`). */
2094
- this.appUriSignal = signal('', /* @ts-ignore */
2095
- ...(ngDevMode ? [{ debugName: "appUriSignal" }] : /* istanbul ignore next */ []));
2096
- /** Writable signal holding the base URI of the backend API service (e.g. `https://api.example.com`). */
2097
- this.appServiceUriSignal = signal('', /* @ts-ignore */
2098
- ...(ngDevMode ? [{ debugName: "appServiceUriSignal" }] : /* istanbul ignore next */ []));
2099
- /**
2100
- * Writable signal holding the redirect URI used after login.
2101
- * When empty, `appLoginRedirectUri` falls back to `appUri`.
2102
- */
2103
- this.appLoginRedirectUriSignal = signal('', /* @ts-ignore */
2104
- ...(ngDevMode ? [{ debugName: "appLoginRedirectUriSignal" }] : /* istanbul ignore next */ []));
2105
- /**
2106
- * Writable signal holding the login endpoint URI of the backend service.
2107
- * When empty, `appServiceLoginUri` falls back to `appServiceUri`.
2108
- */
2109
- this.appServiceLoginUriSignal = signal('', /* @ts-ignore */
2110
- ...(ngDevMode ? [{ debugName: "appServiceLoginUriSignal" }] : /* istanbul ignore next */ []));
2111
- // ── Computed: fallback logic ──────────────────────────────────────────────
2112
- this._effectiveLoginRedirectUri = computed(() => this.appLoginRedirectUriSignal() || this.appUriSignal(), /* @ts-ignore */
2113
- ...(ngDevMode ? [{ debugName: "_effectiveLoginRedirectUri" }] : /* istanbul ignore next */ []));
2114
- this._effectiveServiceLoginUri = computed(() => this.appServiceLoginUriSignal() || this.appServiceUriSignal(), /* @ts-ignore */
2115
- ...(ngDevMode ? [{ debugName: "_effectiveServiceLoginUri" }] : /* istanbul ignore next */ []));
2116
- }
2117
- // ── Getter / setter API (kept for backward compatibility) ─────────────────
2118
- /** Base URI of the frontend application. */
2119
- get appUri() { return this.appUriSignal(); }
2120
- /** @param value - The base URI of the frontend application. */
2121
- set appUri(value) { this.appUriSignal.set(value); }
2122
- /** Base URI of the backend API service. */
2123
- get appServiceUri() { return this.appServiceUriSignal(); }
2124
- /** @param value - The base URI of the backend API service. */
2125
- set appServiceUri(value) { this.appServiceUriSignal.set(value); }
2126
- /**
2127
- * Effective login redirect URI.
2128
- * Returns the explicitly set value, or falls back to `appUri` when empty.
2129
- */
2130
- get appLoginRedirectUri() { return this._effectiveLoginRedirectUri(); }
2131
- /** @param value - The redirect URI to use after login. */
2132
- set appLoginRedirectUri(value) { this.appLoginRedirectUriSignal.set(value); }
2133
- /**
2134
- * Effective service login URI.
2135
- * Returns the explicitly set value, or falls back to `appServiceUri` when empty.
2136
- */
2137
- get appServiceLoginUri() { return this._effectiveServiceLoginUri(); }
2138
- /** @param value - The login endpoint URI of the backend service. */
2139
- set appServiceLoginUri(value) { this.appServiceLoginUriSignal.set(value); }
2140
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: EnvironmentService, deps: [], target: i0.ɵɵFactoryTarget.Service }); }
2141
- static { this.ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.1.7", ngImport: i0, type: EnvironmentService }); }
2142
- }
2143
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: EnvironmentService, decorators: [{
2144
- type: Service
2145
- }] });
2146
-
2147
- /**
2148
- * Service that exposes reactive screen and device-capability information.
2149
- */
2150
- class ScreenService {
2151
- constructor() {
2152
- /**
2153
- * Writable signal that holds the current Material breakpoint alias
2154
- * (e.g. `'xs'`, `'sm'`, `'md'`, `'lg'`, `'xl'`).
2155
- * Updated externally by the breakpoint observer in the consuming component or module.
2156
- */
2157
- this.mq = signal('', /* @ts-ignore */
2158
- ...(ngDevMode ? [{ debugName: "mq" }] : /* istanbul ignore next */ []));
2159
- }
2160
- /**
2161
- * Returns `true` when the primary input mechanism can hover over elements
2162
- * with coarse pointer precision (i.e. a touch screen).
2163
- */
2164
- get isTouchable() {
2165
- return SystemUtils.isTouchable();
2166
- }
2167
- /**
2168
- * Returns `true` when the current browser is Internet Explorer or the legacy Edge (EdgeHTML).
2169
- * Modern Chromium-based Edge reports a different user-agent and will return `false`.
2170
- */
2171
- get isIEOrEdge() {
2172
- return SystemUtils.isBrowser() && /msie\s|trident\/|edge\//i.test(window.navigator.userAgent);
2173
- }
2174
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: ScreenService, deps: [], target: i0.ɵɵFactoryTarget.Service }); }
2175
- static { this.ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.1.7", ngImport: i0, type: ScreenService }); }
2176
- }
2177
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: ScreenService, decorators: [{
2178
- type: Service
2179
- }] });
2180
-
2181
- class SplashService {
2182
- constructor() {
2183
- this.isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
2184
- }
2185
- /**
2186
- * Resolve the global splash API exposed by index.html.
2187
- * @returns The API object, or `undefined` when the splash has already
2188
- * been removed or when running in SSR.
2189
- */
2190
- get api() {
2191
- return this.isBrowser ? window.splash : undefined;
2192
- }
2193
- /**
2194
- * Update the splash message (e.g. "Accesso in corso...").
2195
- * No-op when the splash is gone or in SSR.
2196
- * @param text - The new message to display.
2197
- */
2198
- setMessage(text) { this.api?.setMessage(text); }
2199
- /**
2200
- * Defuse the watchdog: Angular bootstrap succeeded,
2201
- * no automatic reload will ever fire.
2202
- */
2203
- bootstrapped() { this.api?.disarm(); }
2204
- /** Fade out and remove the splash overlay from the DOM. */
2205
- hide() { this.api?.hide(); }
2206
- /**
2207
- * Give up on loading, without taking the splash away.
2208
- *
2209
- * The splash stays where it is but stops pretending something is still happening: the progress
2210
- * bar goes and a reload button appears, so a user who would otherwise sit in front of an endless
2211
- * animation has something to do. Uncovering the page instead would be worse — whatever is
2212
- * underneath is, by definition, not ready.
2213
- *
2214
- * Falls back to {@link setMessage} where `index.html` does not publish `fail`: the user still
2215
- * reads what happened, only without the button.
2216
- * @param text - What to tell the user.
2217
- */
2218
- fail(text) {
2219
- const api = this.api;
2220
- if (!api)
2221
- return;
2222
- if (api.fail) {
2223
- api.fail(text);
2224
- }
2225
- else {
2226
- api.setMessage(text);
2227
- }
2228
- }
2229
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: SplashService, deps: [], target: i0.ɵɵFactoryTarget.Service }); }
2230
- static { this.ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.1.7", ngImport: i0, type: SplashService }); }
2231
- }
2232
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: SplashService, decorators: [{
2233
- type: Service
2234
- }] });
2235
-
2236
- /**
2237
- * Application-level theme management service.
2238
- *
2239
- * Responsibilities:
2240
- * - Persists the user's preferred theme in `localStorage`.
2241
- * - Applies `'light'` or `'dark'` CSS class to `<body>`.
2242
- * - Reacts to OS-level colour-scheme changes when the theme is set to `'auto'`.
2243
- * - Synchronises theme changes across browser tabs via `BroadcastChannel`.
2244
- * - Exposes reactive signals (`themeIcon`, `themeName`, `toggleTooltip`) for the UI.
2245
- */
2246
- class ThemeService {
2247
- constructor() {
2248
- this.broadcastChannel = new BroadcastChannelManager('THEME-SERVICE');
2249
- this.broadcastMessage = '$theme-changed';
2250
- this.prefersColorSchemeMediaQueryList = typeof window !== 'undefined' && typeof window.matchMedia === 'function'
2251
- ? window.matchMedia('(prefers-color-scheme: dark)')
2252
- : undefined;
2253
- this.themeChanged = new BehaviorSubject('auto');
2254
- this.renderFactory2 = inject(RendererFactory2);
2255
- this.renderer = this.renderFactory2.createRenderer(null, null);
2256
- /** The currently selected `ThemeType` (may be `'auto'`). */
2257
- this.theme = signal('light', /* @ts-ignore */
2258
- ...(ngDevMode ? [{ debugName: "theme" }] : /* istanbul ignore next */ []));
2259
- /** `true` when the theme is set to follow the system preference. */
2260
- this.auto = computed(() => this.theme() === 'auto', /* @ts-ignore */
2261
- ...(ngDevMode ? [{ debugName: "auto" }] : /* istanbul ignore next */ []));
2262
- this.themeInfo = {
2263
- auto: { icon: 'routine', name: 'Tema di sistema', next: 'light', tooltip: 'Passa a tema chiaro' },
2264
- light: { icon: 'light_mode', name: 'Tema chiaro', next: 'dark', tooltip: 'Passa a tema scuro' },
2265
- dark: { icon: 'dark_mode', name: 'Tema scuro', next: 'auto', tooltip: 'Passa a tema di sistema' },
2266
- };
2267
- /** Reactive Material icon code for the current theme (use with `<mat-icon>`). */
2268
- this.themeIcon = computed(() => this.themeInfo[this.theme()].icon, /* @ts-ignore */
2269
- ...(ngDevMode ? [{ debugName: "themeIcon" }] : /* istanbul ignore next */ []));
2270
- /** Reactive human-readable name of the current theme. */
2271
- this.themeName = computed(() => this.themeInfo[this.theme()].name, /* @ts-ignore */
2272
- ...(ngDevMode ? [{ debugName: "themeName" }] : /* istanbul ignore next */ []));
2273
- /** Reactive tooltip text for the theme toggle button. */
2274
- this.toggleTooltip = computed(() => this.themeInfo[this.theme()].tooltip, /* @ts-ignore */
2275
- ...(ngDevMode ? [{ debugName: "toggleTooltip" }] : /* istanbul ignore next */ []));
2276
- this.changed = this.themeChanged.asObservable();
2277
- }
2278
- /**
2279
- * Initialises the service: loads the preferred theme, registers OS-change listener,
2280
- * and subscribes to cross-tab broadcast messages.
2281
- * Call this once during application bootstrap (e.g. in `APP_INITIALIZER`).
2282
- * @param theme - The initial theme to apply (defaults to the value stored in `localStorage`).
2283
- */
2284
- initialize(theme = this.getPreferredTheme()) {
2285
- if (this.prefersColorSchemeMediaQueryList) {
2286
- this.prefersColorSchemeMediaQueryList.onchange = () => {
2287
- if (this.auto())
2288
- this.setTheme(this.theme());
2289
- };
2290
- }
2291
- this.broadcastChannel.subscribe({
2292
- messageId: this.broadcastMessage,
2293
- action: (bag) => {
2294
- if (bag.data && this.theme() !== bag.data) {
2295
- this.setTheme(bag.data);
2296
- }
2297
- }
2298
- });
2299
- this.setTheme(theme);
2300
- }
2301
- ngOnDestroy() {
2302
- this.broadcastChannel.dispose();
2303
- }
2304
- /**
2305
- * Advances the theme through the cycle: `auto` → `light` → `dark` → `auto`.
2306
- */
2307
- toggleTheme() {
2308
- this.setTheme(this.themeInfo[this.theme()].next);
2309
- }
2310
- /**
2311
- * Returns the resolved effective theme (`'light'` or `'dark'`), taking the OS preference
2312
- * into account when the current mode is `'auto'`.
2313
- * @returns `'light'` or `'dark'` — never `'auto'`.
2314
- */
2315
- getTheme() {
2316
- const current = this.theme();
2317
- if (current === 'auto') {
2318
- return this.prefersColorSchemeMediaQueryList?.matches ? 'dark' : 'light';
2319
- }
2320
- return current;
2321
- }
2322
- /**
2323
- * Reads the theme stored in `localStorage` and validates it.
2324
- * Falls back to `'auto'` when the stored value is missing or invalid.
2325
- * @returns The persisted `ThemeType`, or `'auto'` as default.
2326
- */
2327
- getPreferredTheme() {
2328
- if (typeof localStorage === 'undefined')
2329
- return 'auto';
2330
- const stored = localStorage.getItem('preferred-theme');
2331
- return stored && this.isTheme(stored) ? stored : 'auto';
2332
- }
2333
- /**
2334
- * Returns `true` when `value` is a valid `ThemeType` string.
2335
- * Acts as a TypeScript type guard.
2336
- * @param value - The string to check.
2337
- */
2338
- isTheme(value) {
2339
- return value === 'auto' || value === 'light' || value === 'dark';
2340
- }
2341
- /**
2342
- * Applies the given theme to `<body>` (CSS class), persists it to `localStorage`,
2343
- * notifies the `changed` observable, and broadcasts the change to other tabs.
2344
- * @param theme - The `ThemeType` to apply (defaults to `'auto'`).
2345
- */
2346
- setTheme(theme = 'auto') {
2347
- this.renderer.removeClass(document.body, 'light');
2348
- this.renderer.removeClass(document.body, 'dark');
2349
- this.theme.set(theme);
2350
- if (typeof localStorage !== 'undefined') {
2351
- localStorage.setItem('preferred-theme', theme);
2352
- }
2353
- const effectiveClass = this.auto()
2354
- ? (this.prefersColorSchemeMediaQueryList?.matches ? 'dark' : 'light')
2355
- : theme;
2356
- this.renderer.addClass(document.body, effectiveClass);
2357
- this.themeChanged.next(this.getTheme());
2358
- this.broadcastChannel.sendMessage(this.broadcastMessage, theme);
2359
- }
2360
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: ThemeService, deps: [], target: i0.ɵɵFactoryTarget.Service }); }
2361
- static { this.ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.1.7", ngImport: i0, type: ThemeService }); }
2362
- }
2363
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: ThemeService, decorators: [{
2364
- type: Service
2365
- }], ctorParameters: () => [] });
2366
-
2367
- /*
2368
- * Public API Surface of @arsedizioni/ars-utils/core
2369
- */
2370
-
2371
- /**
2372
- * Generated bundle index. Do not edit.
2373
- */
2374
-
88
+ var SystemUtils = class SystemUtils {
89
+ static {
90
+ this.collator = new Intl.Collator("it", {
91
+ sensitivity: "base",
92
+ numeric: true
93
+ });
94
+ }
95
+ static arrayFindByKey(array, key, value) {
96
+ if (!array) return void 0;
97
+ for (const item of array) if (item[key] === value) return item;
98
+ }
99
+ static arrayFindIndexByKey(array, key, value) {
100
+ if (!array) return -1;
101
+ for (let i = 0; i < array.length; i++) if (array[i][key] === value) return i;
102
+ return -1;
103
+ }
104
+ static arrayGetValue(array, value) {
105
+ if (!array) return void 0;
106
+ for (const item of array) {
107
+ const i = item;
108
+ if (i["value"] === value) return i["name"] ?? i["id"];
109
+ }
110
+ }
111
+ static arrayToNodes(items, parent) {
112
+ const nodes = [];
113
+ items.forEach((n) => {
114
+ const node = {
115
+ id: n.id.toString(),
116
+ name: n.name,
117
+ count: n.count ? n.count : 0,
118
+ children: void 0,
119
+ parent,
120
+ bag: n
121
+ };
122
+ nodes.push(node);
123
+ node.children = n.children && n.children.length > 0 ? this.arrayToNodes(n.children, node) : [];
124
+ });
125
+ return nodes;
126
+ }
127
+ static arraySortCompare(key, order = "asc") {
128
+ const dir = order === "desc" ? -1 : 1;
129
+ return (a, b) => {
130
+ const varA = a[key];
131
+ const varB = b[key];
132
+ if (varA === varB) return 0;
133
+ if (varA === void 0 || varA === null) return -1 * dir;
134
+ if (varB === void 0 || varB === null) return 1 * dir;
135
+ if (typeof varA === "string" && typeof varB === "string") return SystemUtils.collator.compare(varA, varB) * dir;
136
+ return (varA > varB ? 1 : varA < varB ? -1 : 0) * dir;
137
+ };
138
+ }
139
+ static formatWeight(gr) {
140
+ if (gr > 1e6) return `${(gr / 1e6).toFixed(2)} t`;
141
+ else if (gr > 1e5) return `${(gr / 1e5).toFixed(2)} q`;
142
+ else if (gr > 1e3) return `${(gr / 1e3).toFixed(2)} kg`;
143
+ else return `${gr} gr`;
144
+ }
145
+ static formatFileSize(bytes) {
146
+ const MB = 1048576;
147
+ if (bytes >= MB) return `${(bytes / MB).toFixed(1)} MB`;
148
+ if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)} KB`;
149
+ return `${bytes} byte`;
150
+ }
151
+ static compareString(a, b) {
152
+ return this.collator.compare(a ?? "", b ?? "");
153
+ }
154
+ static capitalize(s) {
155
+ if (!s) return void 0;
156
+ let b = "";
157
+ let cap = true;
158
+ for (const char of s) if (char === " ") {
159
+ b += char;
160
+ cap = true;
161
+ } else if (cap) {
162
+ b += char.toUpperCase();
163
+ cap = false;
164
+ } else b += char.toLowerCase();
165
+ return b;
166
+ }
167
+ static truncate(s, max = 500) {
168
+ if (!s) return void 0;
169
+ if (s.length <= max) return s;
170
+ const i = s.lastIndexOf(" ", max - 1);
171
+ return i > 0 ? s.slice(0, i) : s;
172
+ }
173
+ static join(items, sep = " ", max = 350) {
174
+ if (!items || items.length === 0) return void 0;
175
+ if (items.length > 1) {
176
+ let l = 0;
177
+ let s = "";
178
+ while (s.length < max && items.length > l) {
179
+ if (l > 0) s += sep;
180
+ s += items[l++];
181
+ }
182
+ if (items.length > l) s += "...";
183
+ return s;
184
+ }
185
+ return items[0];
186
+ }
187
+ static normalizeDisplay(s) {
188
+ if (!s) return s;
189
+ return s.split(" ").map((word, wordIndex) => {
190
+ if (!word) return word;
191
+ if (word === word.toUpperCase() && /[A-Z]/.test(word)) return word;
192
+ const chars = word.split("");
193
+ return chars.map((char, charIndex) => {
194
+ if (charIndex === 0 && wordIndex === 0) {
195
+ const secondChar = chars[1] ?? "";
196
+ return char === char.toUpperCase() && secondChar === secondChar.toUpperCase() && /[A-Z]/.test(char) && /[A-Z]/.test(secondChar) ? char : char.toLowerCase();
197
+ }
198
+ return char.toLowerCase();
199
+ }).join("");
200
+ }).join(" ");
201
+ }
202
+ static replaceAsHtml(s) {
203
+ if (!s) return "";
204
+ return s.replace(/https?:\/\/[^\s<]+/gi, (v) => `<a href="${v}" target="_blank" rel="noopener">${v}</a>`);
205
+ }
206
+ static compareNames(a, b) {
207
+ if (a) a = a.trim();
208
+ if (b) b = b.trim();
209
+ if (a && b && a.length !== b.length) return false;
210
+ if (this.compareString(a, b) === 0) return true;
211
+ const p1 = (a ?? "").split(" ");
212
+ const p2 = (b ?? "").split(" ");
213
+ if (p1.length !== p2.length) return false;
214
+ let matches = p1.length;
215
+ const used = new Array(p2.length).fill(false);
216
+ for (const s1 of p1) for (let j = 0; j < p2.length; j++) if (!used[j] && this.compareString(s1, p2[j]) === 0) {
217
+ used[j] = true;
218
+ matches--;
219
+ break;
220
+ }
221
+ return matches === 0;
222
+ }
223
+ static cipher(text, key, reverse = false) {
224
+ if (!text || !key) return void 0;
225
+ const SURROGATE_START = 55296;
226
+ const SURROGATE_SIZE = 2048;
227
+ const SPACE = 1112064;
228
+ const toIndex = (cp) => cp < SURROGATE_START ? cp : cp - SURROGATE_SIZE;
229
+ const toCodePoint = (i) => i < SURROGATE_START ? i : i + SURROGATE_SIZE;
230
+ const keyLen = key.length;
231
+ let result = "";
232
+ let i = 0;
233
+ for (const ch of text) {
234
+ let rotation = key.charCodeAt(i % keyLen);
235
+ if (reverse) rotation = -rotation;
236
+ const rotated = ((toIndex(ch.codePointAt(0)) + rotation) % SPACE + SPACE) % SPACE;
237
+ result += String.fromCodePoint(toCodePoint(rotated));
238
+ i++;
239
+ }
240
+ return result;
241
+ }
242
+ static {
243
+ this.CIPHER_VERSION = 1;
244
+ }
245
+ static {
246
+ this.CIPHER_SALT_LENGTH = 16;
247
+ }
248
+ static {
249
+ this.CIPHER_IV_LENGTH = 12;
250
+ }
251
+ static {
252
+ this.CIPHER_ITERATIONS = 6e5;
253
+ }
254
+ static async deriveKey(password, salt) {
255
+ const material = await crypto.subtle.importKey("raw", new TextEncoder().encode(password), "PBKDF2", false, ["deriveKey"]);
256
+ return crypto.subtle.deriveKey({
257
+ name: "PBKDF2",
258
+ salt,
259
+ iterations: this.CIPHER_ITERATIONS,
260
+ hash: "SHA-256"
261
+ }, material, {
262
+ name: "AES-GCM",
263
+ length: 256
264
+ }, false, ["encrypt", "decrypt"]);
265
+ }
266
+ static toBase64Url(bytes) {
267
+ let binary = "";
268
+ const CHUNK = 32768;
269
+ for (let i = 0; i < bytes.length; i += CHUNK) binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
270
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
271
+ }
272
+ static fromBase64Url(value) {
273
+ const b64 = value.replace(/-/g, "+").replace(/_/g, "/");
274
+ const padded = b64 + "=".repeat((4 - b64.length % 4) % 4);
275
+ const binary = atob(padded);
276
+ const bytes = new Uint8Array(binary.length);
277
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
278
+ return bytes;
279
+ }
280
+ static async encrypt(text, password) {
281
+ if (!text || !password) return void 0;
282
+ if (typeof crypto === "undefined" || !crypto.subtle) return void 0;
283
+ try {
284
+ const salt = crypto.getRandomValues(new Uint8Array(this.CIPHER_SALT_LENGTH));
285
+ const iv = crypto.getRandomValues(new Uint8Array(this.CIPHER_IV_LENGTH));
286
+ const key = await this.deriveKey(password, salt);
287
+ const cipherText = new Uint8Array(await crypto.subtle.encrypt({
288
+ name: "AES-GCM",
289
+ iv
290
+ }, key, new TextEncoder().encode(text)));
291
+ const payload = new Uint8Array(1 + salt.length + iv.length + cipherText.length);
292
+ payload[0] = this.CIPHER_VERSION;
293
+ payload.set(salt, 1);
294
+ payload.set(iv, 1 + salt.length);
295
+ payload.set(cipherText, 1 + salt.length + iv.length);
296
+ return this.toBase64Url(payload);
297
+ } catch {
298
+ return;
299
+ }
300
+ }
301
+ static async decrypt(payload, password) {
302
+ if (!payload || !password) return void 0;
303
+ if (typeof crypto === "undefined" || !crypto.subtle) return void 0;
304
+ try {
305
+ const bytes = this.fromBase64Url(payload);
306
+ const headerLength = 1 + this.CIPHER_SALT_LENGTH + this.CIPHER_IV_LENGTH;
307
+ if (bytes.length <= headerLength || bytes[0] !== this.CIPHER_VERSION) return void 0;
308
+ const salt = bytes.subarray(1, 1 + this.CIPHER_SALT_LENGTH);
309
+ const iv = bytes.subarray(1 + this.CIPHER_SALT_LENGTH, headerLength);
310
+ const cipherText = bytes.subarray(headerLength);
311
+ const key = await this.deriveKey(password, salt);
312
+ const plain = await crypto.subtle.decrypt({
313
+ name: "AES-GCM",
314
+ iv
315
+ }, key, cipherText);
316
+ return new TextDecoder().decode(plain);
317
+ } catch {
318
+ return;
319
+ }
320
+ }
321
+ static clone(obj) {
322
+ if (obj === null || obj === void 0) return obj;
323
+ return structuredClone(obj);
324
+ }
325
+ static deepClone(obj, dest) {
326
+ const cloned = structuredClone(obj);
327
+ if (dest) {
328
+ Object.assign(dest, cloned);
329
+ return dest;
330
+ }
331
+ return cloned;
332
+ }
333
+ static parseUUID(value) {
334
+ if (!value) return false;
335
+ let s = value.trim();
336
+ if (s.startsWith("{") && s.endsWith("}") || s.startsWith("(") && s.endsWith(")")) s = s.slice(1, -1);
337
+ return /^[0-9a-fA-F]{8}-(?:[0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}$/.test(s);
338
+ }
339
+ static parseUUIDNotEmpty(value) {
340
+ return this.parseUUID(value) && !/^[{(]?0{8}-(?:0{4}-){3}0{12}[)}]?$/.test((value ?? "").trim());
341
+ }
342
+ static emptyUUID() {
343
+ return "00000000-0000-0000-0000-000000000000";
344
+ }
345
+ static generateUUID() {
346
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
347
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
348
+ const r = Math.random() * 16 | 0;
349
+ return (c === "x" ? r : r & 3 | 8).toString(16);
350
+ });
351
+ }
352
+ static inflateUUID(value) {
353
+ const s = value.trim();
354
+ if (s.length !== 32) return s;
355
+ return `${s.slice(0, 8)}-${s.slice(8, 12)}-${s.slice(12, 16)}-${s.slice(16, 20)}-${s.slice(20)}`;
356
+ }
357
+ static {
358
+ 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;
359
+ }
360
+ static {
361
+ this.urlRegex = /^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,12})(:\d{1,5})?([/\w .%-]*)?(\?[^\s#]*)?(#\S*)?$/i;
362
+ }
363
+ static parseEmail(value) {
364
+ if (!value) return true;
365
+ return this.emailRegex.test(value.trim().toLowerCase());
366
+ }
367
+ static parseEmails(value) {
368
+ if (!value || value.trim().length === 0) return true;
369
+ const items = value.split(/[;,]/).map((e) => e.trim().toLowerCase()).filter((e) => e.length > 0);
370
+ if (items.length === 0) return false;
371
+ return items.every((e) => this.emailRegex.test(e));
372
+ }
373
+ static parseUrl(value) {
374
+ if (!value) return true;
375
+ return this.urlRegex.test(value.trim().toLowerCase());
376
+ }
377
+ static getDateParts(value) {
378
+ if (!value) return void 0;
379
+ let parts = [];
380
+ if (value.indexOf("-") !== -1) {
381
+ const p = value.split("-");
382
+ if (p.length !== 3) return void 0;
383
+ parts = [
384
+ parseInt(p[0]),
385
+ parseInt(p[1]) - 1,
386
+ parseInt(p[2])
387
+ ];
388
+ } else if (value.indexOf("/") !== -1) {
389
+ const p = value.split("/");
390
+ if (p.length !== 3) return void 0;
391
+ parts = [
392
+ parseInt(p[2]),
393
+ parseInt(p[1]) - 1,
394
+ parseInt(p[0])
395
+ ];
396
+ } else return;
397
+ return parts;
398
+ }
399
+ static isValidDate(d) {
400
+ return d instanceof Date && !isNaN(d.getTime());
401
+ }
402
+ static parseDate(value, locale = it) {
403
+ if (!value) return void 0;
404
+ if (value instanceof Date) return this.isValidDate(value) ? value : void 0;
405
+ let d = parseISO(value);
406
+ if (this.isValidDate(d) && d.getFullYear() > 1750) return d;
407
+ d = parse(value, "dd/MM/yyyy", /* @__PURE__ */ new Date(), { locale });
408
+ if (this.isValidDate(d) && d.getFullYear() > 1750) return d;
409
+ d = parse(value, "yyyy-MM-dd", /* @__PURE__ */ new Date(), { locale });
410
+ if (this.isValidDate(d) && d.getFullYear() > 1750) return d;
411
+ const parts = this.getDateParts(value);
412
+ if (!parts) return void 0;
413
+ if (parts[0] < 100) parts[0] += 2e3;
414
+ if (isNaN(parts[0]) || parts[0] < 0 || parts[0] < 1750) return void 0;
415
+ if (isNaN(parts[1]) || parts[1] < 0) return void 0;
416
+ if (isNaN(parts[2]) || parts[2] < 0) return void 0;
417
+ if (parts[1] > 11) return void 0;
418
+ if (parts[1] === 1 && parts[2] > 29) return void 0;
419
+ else if (parts[1] !== 1 && parts[2] > 31) return void 0;
420
+ return new TZDate(parts[0], parts[1], parts[2], 12);
421
+ }
422
+ static toLocalDate(value) {
423
+ if (value == null || value === "") return void 0;
424
+ const ms = value instanceof Date ? value.getTime() : typeof value === "number" ? value : new Date(value).getTime();
425
+ if (isNaN(ms)) return void 0;
426
+ const t = new TZDate(ms, "Europe/Rome");
427
+ Object.defineProperty(t, "toJSON", {
428
+ value: () => format(t, "yyyy-MM-dd'T'HH:mm:ss"),
429
+ enumerable: false,
430
+ configurable: true,
431
+ writable: true
432
+ });
433
+ return t;
434
+ }
435
+ static formatDate(value, fmt = DateFormat.Short, locale = it) {
436
+ if (!value) return "";
437
+ if (typeof value === "string" || value instanceof String) value = this.parseDate(value);
438
+ if (!this.isValidDate(value)) return "";
439
+ switch (fmt) {
440
+ case DateFormat.Short: return format(value, "dd/MM/yyyy", { locale });
441
+ case DateFormat.Long: return format(value, "d MMMM yyyy", { locale });
442
+ case DateFormat.LongWithShortMonth: return format(value, "d MMM yyyy", { locale });
443
+ case DateFormat.LongWithWeekDay: return format(value, "EEEE, d MMMM yyyy", { locale });
444
+ case DateFormat.LongWithShortWeekDay: return format(value, "EEE, d MMMM yyyy", { locale });
445
+ case DateFormat.MonthAndYear: return format(value, "MMM yyyy", { locale });
446
+ case DateFormat.LongMonthAndYear: return format(value, "MMMM yyyy", { locale });
447
+ case DateFormat.WeekDay: return format(value, "EEE, d", { locale });
448
+ case DateFormat.LongWeekDay: return format(value, "EEEE, d", { locale });
449
+ case DateFormat.DayAndMonth: return format(value, "d MMMM", { locale });
450
+ case DateFormat.ShortUS: return format(value, "MM/dd/yyyy", { locale });
451
+ case DateFormat.ShortISO8601: return format(value, "yyyy-MM-dd", { locale });
452
+ default: return format(value, fmt, { locale });
453
+ }
454
+ }
455
+ static changeDateInterval(value, interval, end = false, copy = false) {
456
+ if (value) {
457
+ let year = -1;
458
+ if (value.length === 4 && (year = parseInt(value)) > 1750) {
459
+ if (!end) {
460
+ interval.from = new TZDate(new Date(year, 0, 1), "Europe/Rome");
461
+ interval.to = new TZDate(new Date(year, 11, 31), "Europe/Rome");
462
+ } else interval.to = new Date(year, 11, 31);
463
+ } else {
464
+ const parts = this.getDateParts(value);
465
+ if (!parts || parts.length !== 3) return;
466
+ if (isNaN(parts[0]) || parts[0] < 1750) return;
467
+ if (isNaN(parts[1]) || parts[1] < 0 || parts[1] > 11) return;
468
+ if (isNaN(parts[2]) || parts[2] < 1 || parts[2] > 31) return;
469
+ const d = new TZDate(new Date(parts[0], parts[1], parts[2]), "Europe/Rome");
470
+ if (end) interval.to = d;
471
+ else if (copy) {
472
+ interval.from = d;
473
+ interval.to = d;
474
+ } else interval.from = d;
475
+ }
476
+ }
477
+ }
478
+ static {
479
+ this.numberFormatCache = /* @__PURE__ */ new Map();
480
+ }
481
+ static getNumberFormat(locale, options) {
482
+ const key = `${locale}|${options.style}|${options.currency ?? ""}|${options.minimumFractionDigits}|${options.maximumFractionDigits}`;
483
+ let fmt = this.numberFormatCache.get(key);
484
+ if (!fmt) {
485
+ fmt = new Intl.NumberFormat(locale, options);
486
+ this.numberFormatCache.set(key, fmt);
487
+ }
488
+ return fmt;
489
+ }
490
+ static formatNumber(value, decimals = 2, locale = "it-IT") {
491
+ return this.getNumberFormat(locale, {
492
+ style: "decimal",
493
+ minimumFractionDigits: 0,
494
+ maximumFractionDigits: decimals
495
+ }).format(value);
496
+ }
497
+ static formatCurrency(value, currency = "EUR", decimals = 2, locale = "it-IT") {
498
+ return this.getNumberFormat(locale, {
499
+ style: "currency",
500
+ currency,
501
+ minimumFractionDigits: 0,
502
+ maximumFractionDigits: decimals
503
+ }).format(value);
504
+ }
505
+ static urlEncode(value) {
506
+ return value.length > 0 ? encodeURIComponent(value) : void 0;
507
+ }
508
+ static urlDecode(value) {
509
+ return value && value.length > 0 ? decodeURIComponent(value.replace(/\+/g, "%20")) : void 0;
510
+ }
511
+ static getQueryStringValueByName(name) {
512
+ if (!this.isBrowser()) return void 0;
513
+ const v = new URLSearchParams(window.location.search).get(name);
514
+ return v && v !== "null" ? v : void 0;
515
+ }
516
+ static generatePassword(length = 12) {
517
+ const lower = "abcdefghijkmnopqrstuvwxyz";
518
+ const upper = "ABCDEFGHJKLMNPQRSTUVWXYZ";
519
+ const digits = "23456789";
520
+ const symbols = "$!#%&*+-";
521
+ const all = lower + upper + digits + symbols;
522
+ const rnd = (max) => {
523
+ if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
524
+ const buf = /* @__PURE__ */ new Uint32Array(1);
525
+ crypto.getRandomValues(buf);
526
+ return buf[0] % max;
527
+ }
528
+ return Math.floor(Math.random() * max);
529
+ };
530
+ const chars = [
531
+ lower[rnd(25)],
532
+ upper[rnd(24)],
533
+ digits[rnd(8)],
534
+ symbols[rnd(8)]
535
+ ];
536
+ while (chars.length < Math.max(length, 8)) chars.push(all[rnd(65)]);
537
+ for (let i = chars.length - 1; i > 0; i--) {
538
+ const j = rnd(i + 1);
539
+ [chars[i], chars[j]] = [chars[j], chars[i]];
540
+ }
541
+ return chars.join("");
542
+ }
543
+ static calculatePasswordStrength(password) {
544
+ if (password && password.length > 0) {
545
+ let score = 0;
546
+ const suggestions = [];
547
+ if (password.length >= 10) score++;
548
+ else suggestions.push("Usa almeno 10 caratteri.");
549
+ if (password.length >= 12) score++;
550
+ else if (password.length >= 10) suggestions.push("Considera di usare più di 12 caratteri.");
551
+ if (/[a-z]/.test(password)) score++;
552
+ else suggestions.push("Aggiungi lettere minuscole.");
553
+ if (/[A-Z]/.test(password)) score++;
554
+ else suggestions.push("Aggiungi lettere maiuscole.");
555
+ if (/\d/.test(password)) score++;
556
+ else suggestions.push("Aggiungi numeri.");
557
+ if (/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password)) score++;
558
+ else suggestions.push("Aggiungi caratteri speciali (!@#$%^&*).");
559
+ if (/(.)\1{2,}/.test(password)) {
560
+ score = Math.max(0, score - 1);
561
+ suggestions.push("Evita di ripetere lo stesso carattere.");
562
+ }
563
+ if (/123|abc|qwe/i.test(password)) {
564
+ score = Math.max(0, score - 1);
565
+ suggestions.push("Evita sequenze comuni (123, abc, qwe).");
566
+ }
567
+ let label;
568
+ let color;
569
+ let isValid;
570
+ if (score <= 2) {
571
+ label = "Molto debole";
572
+ color = "#f44336";
573
+ isValid = false;
574
+ } else if (score <= 3) {
575
+ label = "Debole";
576
+ color = "#ff9800";
577
+ isValid = false;
578
+ } else if (score <= 4) {
579
+ label = "Media";
580
+ color = "#ffc107";
581
+ isValid = true;
582
+ } else if (score <= 5) {
583
+ label = "Forte";
584
+ color = "#8bc34a";
585
+ isValid = true;
586
+ } else {
587
+ label = "Molto forte";
588
+ color = "#4caf50";
589
+ isValid = true;
590
+ }
591
+ return {
592
+ score,
593
+ label,
594
+ color,
595
+ suggestions,
596
+ isValid
597
+ };
598
+ } else return {
599
+ score: 0,
600
+ isValid: false,
601
+ suggestions: []
602
+ };
603
+ }
604
+ static isTouchable() {
605
+ return this.isBrowser() && ("ontouchstart" in window || navigator.maxTouchPoints > 0);
606
+ }
607
+ static isBrowser() {
608
+ return typeof window !== "undefined";
609
+ }
610
+ static toNodes(folders) {
611
+ return this._toNodes(folders, void 0);
612
+ }
613
+ static _toNodes(folders, parent) {
614
+ const nodes = [];
615
+ folders.forEach((n) => {
616
+ const node = {
617
+ id: n.id,
618
+ name: n.name,
619
+ count: n.count,
620
+ parent,
621
+ children: void 0,
622
+ bag: n
623
+ };
624
+ const children = n.children ?? n.subFolders ?? [];
625
+ if (children.length > 0) node.children = this._toNodes(children, node);
626
+ nodes.push(node);
627
+ });
628
+ return nodes;
629
+ }
630
+ static getFlags(value, max = 30) {
631
+ if (value !== 0) {
632
+ const items = [];
633
+ const top = Math.min(max, 30);
634
+ for (let exp = 0; exp <= top; exp++) {
635
+ const v = 1 << exp;
636
+ if ((value & v) === v) items.push(v);
637
+ }
638
+ return items;
639
+ } else return [];
640
+ }
641
+ static {
642
+ this.colorLightCache = /* @__PURE__ */ new Map();
643
+ }
644
+ static isColorLight(color, minimumLuminance = 186) {
645
+ if (!this.isBrowser()) return true;
646
+ const cacheKey = `${color}|${minimumLuminance}`;
647
+ const cached = this.colorLightCache.get(cacheKey);
648
+ if (cached !== void 0) return cached;
649
+ const tempDiv = document.createElement("div");
650
+ tempDiv.style.color = color;
651
+ document.body.appendChild(tempDiv);
652
+ const rgb = getComputedStyle(tempDiv).color;
653
+ document.body.removeChild(tempDiv);
654
+ const result = rgb.match(/\d+/g);
655
+ let light = true;
656
+ if (result && result.length >= 3) {
657
+ const r = parseInt(result[0], 10);
658
+ const g = parseInt(result[1], 10);
659
+ const b = parseInt(result[2], 10);
660
+ light = .299 * r + .587 * g + .114 * b > minimumLuminance;
661
+ }
662
+ this.colorLightCache.set(cacheKey, light);
663
+ return light;
664
+ }
665
+ };
666
+ var SelectableModel = class {
667
+ get all() {
668
+ return this._all();
669
+ }
670
+ get current() {
671
+ return this._current;
672
+ }
673
+ constructor(allowMultiSelect = true, lookupFieldName = "id") {
674
+ this.changed = new EventEmitter();
675
+ this._all = signal([], ...ngDevMode ? [{ debugName: "_all" }] : /* istanbul ignore next */ []);
676
+ this.hasValue = computed(() => this._all().length > 0, ...ngDevMode ? [{ debugName: "hasValue" }] : /* istanbul ignore next */ []);
677
+ this.count = computed(() => this._all().length, ...ngDevMode ? [{ debugName: "count" }] : /* istanbul ignore next */ []);
678
+ this.selectedOne = computed(() => this._all().length === 1, ...ngDevMode ? [{ debugName: "selectedOne" }] : /* istanbul ignore next */ []);
679
+ this.selectedMany = computed(() => this._all().length > 1, ...ngDevMode ? [{ debugName: "selectedMany" }] : /* istanbul ignore next */ []);
680
+ this.selectedAny = computed(() => this._all().length > 0, ...ngDevMode ? [{ debugName: "selectedAny" }] : /* istanbul ignore next */ []);
681
+ this.selectedNone = computed(() => this._all().length === 0, ...ngDevMode ? [{ debugName: "selectedNone" }] : /* istanbul ignore next */ []);
682
+ this.first = computed(() => this._all()[0], ...ngDevMode ? [{ debugName: "first" }] : /* istanbul ignore next */ []);
683
+ this._current = new SelectionModel(allowMultiSelect, [], true, (a, b) => a[lookupFieldName] === b[lookupFieldName]);
684
+ this._lookupFieldName = lookupFieldName;
685
+ }
686
+ updateCurrent(item, lookupValue) {
687
+ if (SystemUtils.arrayFindIndexByKey(this._all(), this._lookupFieldName, lookupValue) !== -1) {
688
+ this._current.toggle(item);
689
+ this.changed.emit(item);
690
+ }
691
+ }
692
+ toggle(item, lookupValue) {
693
+ if (lookupValue === void 0) return;
694
+ const p = SystemUtils.arrayFindIndexByKey(this._all(), this._lookupFieldName, lookupValue);
695
+ if (p === -1) this._all.update((arr) => [...arr, item]);
696
+ else this._all.update((arr) => arr.filter((_, i) => i !== p));
697
+ this._current.toggle(item);
698
+ this.changed.emit(item);
699
+ }
700
+ select(item, lookupValue) {
701
+ if (lookupValue === void 0) return;
702
+ if (SystemUtils.arrayFindIndexByKey(this._all(), this._lookupFieldName, lookupValue) === -1) this._all.update((arr) => [...arr, item]);
703
+ this._current.select(item);
704
+ this.changed.emit(item);
705
+ }
706
+ deselect(item, lookupValue) {
707
+ if (lookupValue === void 0) return;
708
+ const p = SystemUtils.arrayFindIndexByKey(this._all(), this._lookupFieldName, lookupValue);
709
+ if (p !== -1) {
710
+ this._all.update((arr) => arr.filter((_, i) => i !== p));
711
+ this._current.deselect(item);
712
+ this.changed.emit(item);
713
+ }
714
+ }
715
+ deselectByValues(lookupValues) {
716
+ if (lookupValues.length === 0) return;
717
+ for (const key of lookupValues) {
718
+ const p = SystemUtils.arrayFindIndexByKey(this._all(), this._lookupFieldName, key);
719
+ if (p !== -1) {
720
+ const item = this._all()[p];
721
+ this._all.update((arr) => arr.filter((_, i) => i !== p));
722
+ this._current.deselect(item);
723
+ this.changed.emit(item);
724
+ }
725
+ }
726
+ }
727
+ clearCurrent(clearFunc) {
728
+ for (const item of this._current.selected) clearFunc(item);
729
+ this._current.clear();
730
+ this.changed.emit(void 0);
731
+ }
732
+ clear() {
733
+ this._all.set([]);
734
+ this._current.clear();
735
+ this.changed.emit(void 0);
736
+ }
737
+ isSelected(lookupValue) {
738
+ return lookupValue !== void 0 && SystemUtils.arrayFindIndexByKey(this._all(), this._lookupFieldName, lookupValue) !== -1;
739
+ }
740
+ };
741
+ var AutoFocusDirective = class AutoFocusDirective {
742
+ constructor() {
743
+ this.elementRef = inject(ElementRef);
744
+ afterNextRender(() => {
745
+ this.elementRef.nativeElement?.focus();
746
+ });
747
+ }
748
+ static {
749
+ this.ɵfac = i0.ɵɵngDeclareFactory({
750
+ minVersion: "12.0.0",
751
+ version: "22.2.1",
752
+ ngImport: i0,
753
+ type: AutoFocusDirective,
754
+ deps: [],
755
+ target: i0.ɵɵFactoryTarget.Directive
756
+ });
757
+ }
758
+ static {
759
+ this.ɵdir = i0.ɵɵngDeclareDirective({
760
+ minVersion: "14.0.0",
761
+ version: "22.2.1",
762
+ type: AutoFocusDirective,
763
+ isStandalone: true,
764
+ selector: "[autoFocus]",
765
+ ngImport: i0
766
+ });
767
+ }
768
+ };
769
+ i0.ɵɵngDeclareClassMetadata({
770
+ minVersion: "12.0.0",
771
+ version: "22.2.1",
772
+ ngImport: i0,
773
+ type: AutoFocusDirective,
774
+ decorators: [{
775
+ type: Directive,
776
+ args: [{
777
+ selector: "[autoFocus]",
778
+ standalone: true
779
+ }]
780
+ }],
781
+ ctorParameters: () => []
782
+ });
783
+ var DateIntervalChangeDirective = class DateIntervalChangeDirective {
784
+ constructor() {
785
+ this.dateIntervalChange = input(new DateInterval(), ...ngDevMode ? [{ debugName: "dateIntervalChange" }] : /* istanbul ignore next */ []);
786
+ this.end = input(false, ...ngDevMode ? [{ debugName: "end" }] : /* istanbul ignore next */ []);
787
+ this.subject = new Subject();
788
+ this.destroyRef = inject(DestroyRef);
789
+ this.subject.pipe(debounceTime(100), takeUntilDestroyed(this.destroyRef)).subscribe((e) => {
790
+ const value = e.target?.value;
791
+ if (value !== void 0) SystemUtils.changeDateInterval(value, this.dateIntervalChange(), this.end(), e.key === " ");
792
+ });
793
+ }
794
+ onKeydown(e) {
795
+ if (e.key === " ") {
796
+ e.preventDefault();
797
+ e.stopPropagation();
798
+ }
799
+ }
800
+ onKeyup(e) {
801
+ this.subject.next(e);
802
+ }
803
+ static {
804
+ this.ɵfac = i0.ɵɵngDeclareFactory({
805
+ minVersion: "12.0.0",
806
+ version: "22.2.1",
807
+ ngImport: i0,
808
+ type: DateIntervalChangeDirective,
809
+ deps: [],
810
+ target: i0.ɵɵFactoryTarget.Directive
811
+ });
812
+ }
813
+ static {
814
+ this.ɵdir = i0.ɵɵngDeclareDirective({
815
+ minVersion: "17.1.0",
816
+ version: "22.2.1",
817
+ type: DateIntervalChangeDirective,
818
+ isStandalone: true,
819
+ selector: "[dateIntervalChange]",
820
+ inputs: {
821
+ dateIntervalChange: {
822
+ classPropertyName: "dateIntervalChange",
823
+ publicName: "dateIntervalChange",
824
+ isSignal: true,
825
+ isRequired: false,
826
+ transformFunction: null
827
+ },
828
+ end: {
829
+ classPropertyName: "end",
830
+ publicName: "end",
831
+ isSignal: true,
832
+ isRequired: false,
833
+ transformFunction: null
834
+ }
835
+ },
836
+ host: { listeners: {
837
+ "keydown": "onKeydown($event)",
838
+ "keyup": "onKeyup($event)"
839
+ } },
840
+ ngImport: i0
841
+ });
842
+ }
843
+ };
844
+ i0.ɵɵngDeclareClassMetadata({
845
+ minVersion: "12.0.0",
846
+ version: "22.2.1",
847
+ ngImport: i0,
848
+ type: DateIntervalChangeDirective,
849
+ decorators: [{
850
+ type: Directive,
851
+ args: [{
852
+ selector: "[dateIntervalChange]",
853
+ standalone: true
854
+ }]
855
+ }],
856
+ ctorParameters: () => [],
857
+ propDecorators: {
858
+ dateIntervalChange: [{
859
+ type: i0.Input,
860
+ args: [{
861
+ isSignal: true,
862
+ alias: "dateIntervalChange",
863
+ required: false
864
+ }]
865
+ }],
866
+ end: [{
867
+ type: i0.Input,
868
+ args: [{
869
+ isSignal: true,
870
+ alias: "end",
871
+ required: false
872
+ }]
873
+ }],
874
+ onKeydown: [{
875
+ type: HostListener,
876
+ args: ["keydown", ["$event"]]
877
+ }],
878
+ onKeyup: [{
879
+ type: HostListener,
880
+ args: ["keyup", ["$event"]]
881
+ }]
882
+ }
883
+ });
884
+ var CopyClipboardDirective = class CopyClipboardDirective {
885
+ constructor() {
886
+ this.payload = input(void 0, {
887
+ ...ngDevMode ? { debugName: "payload" } : /* istanbul ignore next */ {},
888
+ alias: "copyClipboard"
889
+ });
890
+ this.copied = output({ alias: "copied" });
891
+ }
892
+ onClick(e) {
893
+ e.preventDefault();
894
+ const payload = this.payload();
895
+ if (!payload) return;
896
+ if (SystemUtils.isBrowser()) {
897
+ const listener = (clipEvent) => {
898
+ clipEvent.clipboardData?.setData("text/html", payload);
899
+ clipEvent.clipboardData?.setData("text/plain", payload);
900
+ clipEvent.preventDefault();
901
+ this.copied.emit(payload);
902
+ };
903
+ document.addEventListener("copy", listener, false);
904
+ document.execCommand("copy");
905
+ document.removeEventListener("copy", listener, false);
906
+ }
907
+ }
908
+ static {
909
+ this.ɵfac = i0.ɵɵngDeclareFactory({
910
+ minVersion: "12.0.0",
911
+ version: "22.2.1",
912
+ ngImport: i0,
913
+ type: CopyClipboardDirective,
914
+ deps: [],
915
+ target: i0.ɵɵFactoryTarget.Directive
916
+ });
917
+ }
918
+ static {
919
+ this.ɵdir = i0.ɵɵngDeclareDirective({
920
+ minVersion: "17.1.0",
921
+ version: "22.2.1",
922
+ type: CopyClipboardDirective,
923
+ isStandalone: true,
924
+ selector: "[copyClipboard]",
925
+ inputs: { payload: {
926
+ classPropertyName: "payload",
927
+ publicName: "copyClipboard",
928
+ isSignal: true,
929
+ isRequired: false,
930
+ transformFunction: null
931
+ } },
932
+ outputs: { copied: "copied" },
933
+ host: { listeners: { "click": "onClick($event)" } },
934
+ ngImport: i0
935
+ });
936
+ }
937
+ };
938
+ i0.ɵɵngDeclareClassMetadata({
939
+ minVersion: "12.0.0",
940
+ version: "22.2.1",
941
+ ngImport: i0,
942
+ type: CopyClipboardDirective,
943
+ decorators: [{
944
+ type: Directive,
945
+ args: [{
946
+ selector: "[copyClipboard]",
947
+ standalone: true
948
+ }]
949
+ }],
950
+ propDecorators: {
951
+ payload: [{
952
+ type: i0.Input,
953
+ args: [{
954
+ isSignal: true,
955
+ alias: "copyClipboard",
956
+ required: false
957
+ }]
958
+ }],
959
+ copied: [{
960
+ type: i0.Output,
961
+ args: ["copied"]
962
+ }],
963
+ onClick: [{
964
+ type: HostListener,
965
+ args: ["click", ["$event"]]
966
+ }]
967
+ }
968
+ });
969
+ var RemoveFocusDirective = class RemoveFocusDirective {
970
+ constructor() {
971
+ this.elementRef = inject(ElementRef);
972
+ }
973
+ onClick() {
974
+ const el = this.elementRef.nativeElement;
975
+ if (el && typeof el.blur === "function") setTimeout(() => el.blur(), 0);
976
+ }
977
+ static {
978
+ this.ɵfac = i0.ɵɵngDeclareFactory({
979
+ minVersion: "12.0.0",
980
+ version: "22.2.1",
981
+ ngImport: i0,
982
+ type: RemoveFocusDirective,
983
+ deps: [],
984
+ target: i0.ɵɵFactoryTarget.Directive
985
+ });
986
+ }
987
+ static {
988
+ this.ɵdir = i0.ɵɵngDeclareDirective({
989
+ minVersion: "14.0.0",
990
+ version: "22.2.1",
991
+ type: RemoveFocusDirective,
992
+ isStandalone: true,
993
+ selector: "[removeFocus]",
994
+ host: { listeners: { "click": "onClick()" } },
995
+ ngImport: i0
996
+ });
997
+ }
998
+ };
999
+ i0.ɵɵngDeclareClassMetadata({
1000
+ minVersion: "12.0.0",
1001
+ version: "22.2.1",
1002
+ ngImport: i0,
1003
+ type: RemoveFocusDirective,
1004
+ decorators: [{
1005
+ type: Directive,
1006
+ args: [{
1007
+ selector: "[removeFocus]",
1008
+ standalone: true
1009
+ }]
1010
+ }],
1011
+ propDecorators: { onClick: [{
1012
+ type: HostListener,
1013
+ args: ["click"]
1014
+ }] }
1015
+ });
1016
+ var FormatHtmlPipe = class FormatHtmlPipe {
1017
+ constructor() {
1018
+ this.sanitizer = inject(DomSanitizer);
1019
+ }
1020
+ transform(value) {
1021
+ const escaped = (value ?? "").replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;");
1022
+ return this.sanitizer.bypassSecurityTrustHtml(escaped.replaceAll(/(?:\r\n|\r|\n)/g, "<br>"));
1023
+ }
1024
+ static {
1025
+ this.ɵfac = i0.ɵɵngDeclareFactory({
1026
+ minVersion: "12.0.0",
1027
+ version: "22.2.1",
1028
+ ngImport: i0,
1029
+ type: FormatHtmlPipe,
1030
+ deps: [],
1031
+ target: i0.ɵɵFactoryTarget.Pipe
1032
+ });
1033
+ }
1034
+ static {
1035
+ this.ɵpipe = i0.ɵɵngDeclarePipe({
1036
+ minVersion: "14.0.0",
1037
+ version: "22.2.1",
1038
+ ngImport: i0,
1039
+ type: FormatHtmlPipe,
1040
+ isStandalone: true,
1041
+ name: "formatHtml"
1042
+ });
1043
+ }
1044
+ };
1045
+ i0.ɵɵngDeclareClassMetadata({
1046
+ minVersion: "12.0.0",
1047
+ version: "22.2.1",
1048
+ ngImport: i0,
1049
+ type: FormatHtmlPipe,
1050
+ decorators: [{
1051
+ type: Pipe,
1052
+ args: [{
1053
+ name: "formatHtml",
1054
+ standalone: true
1055
+ }]
1056
+ }]
1057
+ });
1058
+ var FormatPipe = class FormatPipe {
1059
+ static {
1060
+ this.currencyFormat = new Intl.NumberFormat("it-IT", {
1061
+ style: "currency",
1062
+ currency: "EUR"
1063
+ });
1064
+ }
1065
+ static {
1066
+ this.numberFormat = new Intl.NumberFormat("it-IT", {
1067
+ style: "decimal",
1068
+ minimumFractionDigits: 0,
1069
+ maximumFractionDigits: 2
1070
+ });
1071
+ }
1072
+ static {
1073
+ this.number0Format = new Intl.NumberFormat("it-IT", {
1074
+ style: "decimal",
1075
+ minimumFractionDigits: 0,
1076
+ maximumFractionDigits: 0
1077
+ });
1078
+ }
1079
+ static {
1080
+ this.percentFormat = new Intl.NumberFormat("it-IT", {
1081
+ style: "percent",
1082
+ minimumFractionDigits: 0,
1083
+ maximumFractionDigits: 0
1084
+ });
1085
+ }
1086
+ transform(value, type = "date", pattern = "dd/MM/yyyy") {
1087
+ if (value === void 0 || value === null) return void 0;
1088
+ switch (type) {
1089
+ case "D":
1090
+ case "date": {
1091
+ const d = SystemUtils.parseDate(value, it);
1092
+ if (d) return format(d, pattern, { locale: it });
1093
+ break;
1094
+ }
1095
+ case "C":
1096
+ case "currency": return FormatPipe.currencyFormat.format(value);
1097
+ case "N":
1098
+ case "number": return FormatPipe.numberFormat.format(value);
1099
+ case "N0":
1100
+ case "number0": return FormatPipe.number0Format.format(value);
1101
+ case "P":
1102
+ case "percentage": return FormatPipe.percentFormat.format(value);
1103
+ }
1104
+ }
1105
+ static {
1106
+ this.ɵfac = i0.ɵɵngDeclareFactory({
1107
+ minVersion: "12.0.0",
1108
+ version: "22.2.1",
1109
+ ngImport: i0,
1110
+ type: FormatPipe,
1111
+ deps: [],
1112
+ target: i0.ɵɵFactoryTarget.Pipe
1113
+ });
1114
+ }
1115
+ static {
1116
+ this.ɵpipe = i0.ɵɵngDeclarePipe({
1117
+ minVersion: "14.0.0",
1118
+ version: "22.2.1",
1119
+ ngImport: i0,
1120
+ type: FormatPipe,
1121
+ isStandalone: true,
1122
+ name: "format"
1123
+ });
1124
+ }
1125
+ };
1126
+ i0.ɵɵngDeclareClassMetadata({
1127
+ minVersion: "12.0.0",
1128
+ version: "22.2.1",
1129
+ ngImport: i0,
1130
+ type: FormatPipe,
1131
+ decorators: [{
1132
+ type: Pipe,
1133
+ args: [{
1134
+ name: "format",
1135
+ standalone: true
1136
+ }]
1137
+ }]
1138
+ });
1139
+ var ReplacePipe = class ReplacePipe {
1140
+ constructor() {
1141
+ this.sanitizer = inject(DomSanitizer);
1142
+ }
1143
+ transform(value, regexValue, replaceValue) {
1144
+ if (!value) return void 0;
1145
+ const replacement = regexValue === "\n" && !replaceValue ? "<br>" : replaceValue ?? "";
1146
+ return this.sanitizer.bypassSecurityTrustHtml(value.replace(new RegExp(regexValue, "g"), replacement));
1147
+ }
1148
+ static {
1149
+ this.ɵfac = i0.ɵɵngDeclareFactory({
1150
+ minVersion: "12.0.0",
1151
+ version: "22.2.1",
1152
+ ngImport: i0,
1153
+ type: ReplacePipe,
1154
+ deps: [],
1155
+ target: i0.ɵɵFactoryTarget.Pipe
1156
+ });
1157
+ }
1158
+ static {
1159
+ this.ɵpipe = i0.ɵɵngDeclarePipe({
1160
+ minVersion: "14.0.0",
1161
+ version: "22.2.1",
1162
+ ngImport: i0,
1163
+ type: ReplacePipe,
1164
+ isStandalone: true,
1165
+ name: "replace"
1166
+ });
1167
+ }
1168
+ };
1169
+ i0.ɵɵngDeclareClassMetadata({
1170
+ minVersion: "12.0.0",
1171
+ version: "22.2.1",
1172
+ ngImport: i0,
1173
+ type: ReplacePipe,
1174
+ decorators: [{
1175
+ type: Pipe,
1176
+ args: [{
1177
+ name: "replace",
1178
+ standalone: true
1179
+ }]
1180
+ }]
1181
+ });
1182
+ var SafeHtmlPipe = class SafeHtmlPipe {
1183
+ constructor() {
1184
+ this.sanitizer = inject(DomSanitizer);
1185
+ }
1186
+ transform(value) {
1187
+ return this.sanitizer.bypassSecurityTrustHtml(value ?? "");
1188
+ }
1189
+ static {
1190
+ this.ɵfac = i0.ɵɵngDeclareFactory({
1191
+ minVersion: "12.0.0",
1192
+ version: "22.2.1",
1193
+ ngImport: i0,
1194
+ type: SafeHtmlPipe,
1195
+ deps: [],
1196
+ target: i0.ɵɵFactoryTarget.Pipe
1197
+ });
1198
+ }
1199
+ static {
1200
+ this.ɵpipe = i0.ɵɵngDeclarePipe({
1201
+ minVersion: "14.0.0",
1202
+ version: "22.2.1",
1203
+ ngImport: i0,
1204
+ type: SafeHtmlPipe,
1205
+ isStandalone: true,
1206
+ name: "safeHtml"
1207
+ });
1208
+ }
1209
+ };
1210
+ i0.ɵɵngDeclareClassMetadata({
1211
+ minVersion: "12.0.0",
1212
+ version: "22.2.1",
1213
+ ngImport: i0,
1214
+ type: SafeHtmlPipe,
1215
+ decorators: [{
1216
+ type: Pipe,
1217
+ args: [{
1218
+ name: "safeHtml",
1219
+ standalone: true
1220
+ }]
1221
+ }]
1222
+ });
1223
+ var SafeUrlPipe = class SafeUrlPipe {
1224
+ constructor() {
1225
+ this.sanitizer = inject(DomSanitizer);
1226
+ }
1227
+ transform(value) {
1228
+ return this.sanitizer.bypassSecurityTrustResourceUrl(value ?? "");
1229
+ }
1230
+ static {
1231
+ this.ɵfac = i0.ɵɵngDeclareFactory({
1232
+ minVersion: "12.0.0",
1233
+ version: "22.2.1",
1234
+ ngImport: i0,
1235
+ type: SafeUrlPipe,
1236
+ deps: [],
1237
+ target: i0.ɵɵFactoryTarget.Pipe
1238
+ });
1239
+ }
1240
+ static {
1241
+ this.ɵpipe = i0.ɵɵngDeclarePipe({
1242
+ minVersion: "14.0.0",
1243
+ version: "22.2.1",
1244
+ ngImport: i0,
1245
+ type: SafeUrlPipe,
1246
+ isStandalone: true,
1247
+ name: "safeUrl"
1248
+ });
1249
+ }
1250
+ };
1251
+ i0.ɵɵngDeclareClassMetadata({
1252
+ minVersion: "12.0.0",
1253
+ version: "22.2.1",
1254
+ ngImport: i0,
1255
+ type: SafeUrlPipe,
1256
+ decorators: [{
1257
+ type: Pipe,
1258
+ args: [{
1259
+ name: "safeUrl",
1260
+ standalone: true
1261
+ }]
1262
+ }]
1263
+ });
1264
+ var SearchCallbackPipe = class SearchCallbackPipe {
1265
+ transform(items, callback) {
1266
+ if (!items || !callback) return items;
1267
+ return items.filter((item) => callback(item));
1268
+ }
1269
+ static {
1270
+ this.ɵfac = i0.ɵɵngDeclareFactory({
1271
+ minVersion: "12.0.0",
1272
+ version: "22.2.1",
1273
+ ngImport: i0,
1274
+ type: SearchCallbackPipe,
1275
+ deps: [],
1276
+ target: i0.ɵɵFactoryTarget.Pipe
1277
+ });
1278
+ }
1279
+ static {
1280
+ this.ɵpipe = i0.ɵɵngDeclarePipe({
1281
+ minVersion: "14.0.0",
1282
+ version: "22.2.1",
1283
+ ngImport: i0,
1284
+ type: SearchCallbackPipe,
1285
+ isStandalone: true,
1286
+ name: "callback",
1287
+ pure: false
1288
+ });
1289
+ }
1290
+ };
1291
+ i0.ɵɵngDeclareClassMetadata({
1292
+ minVersion: "12.0.0",
1293
+ version: "22.2.1",
1294
+ ngImport: i0,
1295
+ type: SearchCallbackPipe,
1296
+ decorators: [{
1297
+ type: Pipe,
1298
+ args: [{
1299
+ name: "callback",
1300
+ pure: false,
1301
+ standalone: true
1302
+ }]
1303
+ }]
1304
+ });
1305
+ var SearchFilterPipe = class SearchFilterPipe {
1306
+ transform(items, value, metadata) {
1307
+ metadata ??= {
1308
+ total: 0,
1309
+ count: 0
1310
+ };
1311
+ if (!items || !value) {
1312
+ metadata.total = items?.length ?? 0;
1313
+ metadata.count = metadata.total;
1314
+ return items;
1315
+ }
1316
+ const query = value.toLowerCase();
1317
+ const result = items.filter((item) => {
1318
+ if (!item) return false;
1319
+ return (item.searchBag?.name ?? (typeof item === "string" ? item : void 0))?.toLowerCase().includes(query) ?? false;
1320
+ });
1321
+ metadata.total = items.length;
1322
+ metadata.count = result.length;
1323
+ return result;
1324
+ }
1325
+ static {
1326
+ this.ɵfac = i0.ɵɵngDeclareFactory({
1327
+ minVersion: "12.0.0",
1328
+ version: "22.2.1",
1329
+ ngImport: i0,
1330
+ type: SearchFilterPipe,
1331
+ deps: [],
1332
+ target: i0.ɵɵFactoryTarget.Pipe
1333
+ });
1334
+ }
1335
+ static {
1336
+ this.ɵpipe = i0.ɵɵngDeclarePipe({
1337
+ minVersion: "14.0.0",
1338
+ version: "22.2.1",
1339
+ ngImport: i0,
1340
+ type: SearchFilterPipe,
1341
+ isStandalone: true,
1342
+ name: "search"
1343
+ });
1344
+ }
1345
+ };
1346
+ i0.ɵɵngDeclareClassMetadata({
1347
+ minVersion: "12.0.0",
1348
+ version: "22.2.1",
1349
+ ngImport: i0,
1350
+ type: SearchFilterPipe,
1351
+ decorators: [{
1352
+ type: Pipe,
1353
+ args: [{
1354
+ name: "search",
1355
+ standalone: true
1356
+ }]
1357
+ }]
1358
+ });
1359
+ var BroadcastChannelManager = class {
1360
+ get currentBus() {
1361
+ return this.channel?.name;
1362
+ }
1363
+ get messages() {
1364
+ return this.subject.asObservable();
1365
+ }
1366
+ observe(messageId) {
1367
+ return this.subject.pipe(filter$1((bag) => bag.messageId === messageId), map$1((bag) => bag));
1368
+ }
1369
+ constructor(bus = "ARS-CHANNEL") {
1370
+ this.subject = new Subject();
1371
+ this.subscriptions = [];
1372
+ if (typeof window !== "undefined" && "BroadcastChannel" in window) {
1373
+ this.channel = new BroadcastChannel(bus);
1374
+ this.channel.onmessageerror = (e) => {
1375
+ console.error("[BroadcastChannelManager] Message receive error", e);
1376
+ };
1377
+ this.channel.onmessage = (e) => {
1378
+ const bag = e.data;
1379
+ if (!bag) return;
1380
+ this.subject.next(bag);
1381
+ this.subscriptions.find((m) => m.messageId === bag.messageId)?.action(bag);
1382
+ };
1383
+ } else console.error("[BroadcastChannelManager] BroadcastChannel API is not supported in this browser");
1384
+ }
1385
+ dispose() {
1386
+ setTimeout(() => {
1387
+ this.subscriptions = [];
1388
+ this.channel?.close();
1389
+ this.channel = void 0;
1390
+ this.subject.complete();
1391
+ }, 1e3);
1392
+ }
1393
+ sendMessage(messageIdOrBag, dataOrDelay, delay) {
1394
+ let bag;
1395
+ let effectiveDelay;
1396
+ if (typeof messageIdOrBag === "string") {
1397
+ bag = {
1398
+ messageId: messageIdOrBag,
1399
+ data: dataOrDelay
1400
+ };
1401
+ effectiveDelay = delay ?? 0;
1402
+ } else {
1403
+ bag = messageIdOrBag;
1404
+ effectiveDelay = typeof dataOrDelay === "number" ? dataOrDelay : 0;
1405
+ }
1406
+ const post = () => this.channel?.postMessage(bag);
1407
+ if (effectiveDelay > 0) setTimeout(post, effectiveDelay);
1408
+ else post();
1409
+ }
1410
+ subscribe(args) {
1411
+ const i = this.subscriptions.findIndex((m) => m.messageId === args.messageId);
1412
+ if (i === -1) this.subscriptions.push(args);
1413
+ else this.subscriptions[i].action = args.action;
1414
+ }
1415
+ unsubscribe(messageId) {
1416
+ const i = this.subscriptions.findIndex((m) => m.messageId === messageId);
1417
+ if (i !== -1) this.subscriptions.splice(i, 1);
1418
+ }
1419
+ unsubscribeAll() {
1420
+ this.subscriptions = [];
1421
+ }
1422
+ };
1423
+ const CHANNEL_NAME = "D2693418-B18F-41BB-BAE9-41BB5407957A-CHANNEL";
1424
+ var BroadcastService = class BroadcastService {
1425
+ constructor() {
1426
+ this.subject = new Subject();
1427
+ this.channel = new BroadcastChannelManager(CHANNEL_NAME);
1428
+ }
1429
+ static createChannel() {
1430
+ return new BroadcastChannelManager(CHANNEL_NAME);
1431
+ }
1432
+ ngOnDestroy() {
1433
+ this.channel?.dispose();
1434
+ this.subject.complete();
1435
+ }
1436
+ sendMessage(id, data, delay = 0) {
1437
+ if (delay > 0) setTimeout(() => this.subject.next({
1438
+ id,
1439
+ data
1440
+ }), delay);
1441
+ else this.subject.next({
1442
+ id,
1443
+ data
1444
+ });
1445
+ }
1446
+ sendChannelMessage(id, data, delay) {
1447
+ this.channel.sendMessage(id, data, delay);
1448
+ }
1449
+ subscribeChannelMessage(id, action) {
1450
+ this.channel.subscribe({
1451
+ messageId: id,
1452
+ action
1453
+ });
1454
+ }
1455
+ unsubscribeChannelMessage(id) {
1456
+ this.channel.unsubscribe(id);
1457
+ }
1458
+ observeChannelMessage(id) {
1459
+ return this.channel.observe(id);
1460
+ }
1461
+ getMessage() {
1462
+ return this.subject.asObservable();
1463
+ }
1464
+ observeMessage(id) {
1465
+ return this.subject.pipe(filter((info) => info.id === id), map((info) => info.data));
1466
+ }
1467
+ static {
1468
+ this.ɵfac = i0.ɵɵngDeclareFactory({
1469
+ minVersion: "12.0.0",
1470
+ version: "22.2.1",
1471
+ ngImport: i0,
1472
+ type: BroadcastService,
1473
+ deps: [],
1474
+ target: i0.ɵɵFactoryTarget.Service
1475
+ });
1476
+ }
1477
+ static {
1478
+ this.ɵprov = i0.ɵɵngDeclareService({
1479
+ minVersion: "22.0.0",
1480
+ version: "22.2.1",
1481
+ ngImport: i0,
1482
+ type: BroadcastService
1483
+ });
1484
+ }
1485
+ };
1486
+ i0.ɵɵngDeclareClassMetadata({
1487
+ minVersion: "12.0.0",
1488
+ version: "22.2.1",
1489
+ ngImport: i0,
1490
+ type: BroadcastService,
1491
+ decorators: [{ type: Service }]
1492
+ });
1493
+ var EnvironmentService = class EnvironmentService {
1494
+ constructor() {
1495
+ this.appUriSignal = signal("", ...ngDevMode ? [{ debugName: "appUriSignal" }] : /* istanbul ignore next */ []);
1496
+ this.appServiceUriSignal = signal("", ...ngDevMode ? [{ debugName: "appServiceUriSignal" }] : /* istanbul ignore next */ []);
1497
+ this.appLoginRedirectUriSignal = signal("", ...ngDevMode ? [{ debugName: "appLoginRedirectUriSignal" }] : /* istanbul ignore next */ []);
1498
+ this.appServiceLoginUriSignal = signal("", ...ngDevMode ? [{ debugName: "appServiceLoginUriSignal" }] : /* istanbul ignore next */ []);
1499
+ this._effectiveLoginRedirectUri = computed(() => this.appLoginRedirectUriSignal() || this.appUriSignal(), ...ngDevMode ? [{ debugName: "_effectiveLoginRedirectUri" }] : /* istanbul ignore next */ []);
1500
+ this._effectiveServiceLoginUri = computed(() => this.appServiceLoginUriSignal() || this.appServiceUriSignal(), ...ngDevMode ? [{ debugName: "_effectiveServiceLoginUri" }] : /* istanbul ignore next */ []);
1501
+ }
1502
+ get appUri() {
1503
+ return this.appUriSignal();
1504
+ }
1505
+ set appUri(value) {
1506
+ this.appUriSignal.set(value);
1507
+ }
1508
+ get appServiceUri() {
1509
+ return this.appServiceUriSignal();
1510
+ }
1511
+ set appServiceUri(value) {
1512
+ this.appServiceUriSignal.set(value);
1513
+ }
1514
+ get appLoginRedirectUri() {
1515
+ return this._effectiveLoginRedirectUri();
1516
+ }
1517
+ set appLoginRedirectUri(value) {
1518
+ this.appLoginRedirectUriSignal.set(value);
1519
+ }
1520
+ get appServiceLoginUri() {
1521
+ return this._effectiveServiceLoginUri();
1522
+ }
1523
+ set appServiceLoginUri(value) {
1524
+ this.appServiceLoginUriSignal.set(value);
1525
+ }
1526
+ static {
1527
+ this.ɵfac = i0.ɵɵngDeclareFactory({
1528
+ minVersion: "12.0.0",
1529
+ version: "22.2.1",
1530
+ ngImport: i0,
1531
+ type: EnvironmentService,
1532
+ deps: [],
1533
+ target: i0.ɵɵFactoryTarget.Service
1534
+ });
1535
+ }
1536
+ static {
1537
+ this.ɵprov = i0.ɵɵngDeclareService({
1538
+ minVersion: "22.0.0",
1539
+ version: "22.2.1",
1540
+ ngImport: i0,
1541
+ type: EnvironmentService
1542
+ });
1543
+ }
1544
+ };
1545
+ i0.ɵɵngDeclareClassMetadata({
1546
+ minVersion: "12.0.0",
1547
+ version: "22.2.1",
1548
+ ngImport: i0,
1549
+ type: EnvironmentService,
1550
+ decorators: [{ type: Service }]
1551
+ });
1552
+ var ScreenService = class ScreenService {
1553
+ constructor() {
1554
+ this.mq = signal("", ...ngDevMode ? [{ debugName: "mq" }] : /* istanbul ignore next */ []);
1555
+ }
1556
+ get isTouchable() {
1557
+ return SystemUtils.isTouchable();
1558
+ }
1559
+ get isIEOrEdge() {
1560
+ return SystemUtils.isBrowser() && /msie\s|trident\/|edge\//i.test(window.navigator.userAgent);
1561
+ }
1562
+ static {
1563
+ this.ɵfac = i0.ɵɵngDeclareFactory({
1564
+ minVersion: "12.0.0",
1565
+ version: "22.2.1",
1566
+ ngImport: i0,
1567
+ type: ScreenService,
1568
+ deps: [],
1569
+ target: i0.ɵɵFactoryTarget.Service
1570
+ });
1571
+ }
1572
+ static {
1573
+ this.ɵprov = i0.ɵɵngDeclareService({
1574
+ minVersion: "22.0.0",
1575
+ version: "22.2.1",
1576
+ ngImport: i0,
1577
+ type: ScreenService
1578
+ });
1579
+ }
1580
+ };
1581
+ i0.ɵɵngDeclareClassMetadata({
1582
+ minVersion: "12.0.0",
1583
+ version: "22.2.1",
1584
+ ngImport: i0,
1585
+ type: ScreenService,
1586
+ decorators: [{ type: Service }]
1587
+ });
1588
+ var SplashService = class SplashService {
1589
+ constructor() {
1590
+ this.isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
1591
+ }
1592
+ get api() {
1593
+ return this.isBrowser ? window.splash : void 0;
1594
+ }
1595
+ setMessage(text) {
1596
+ this.api?.setMessage(text);
1597
+ }
1598
+ bootstrapped() {
1599
+ this.api?.disarm();
1600
+ }
1601
+ hide() {
1602
+ this.api?.hide();
1603
+ }
1604
+ fail(text) {
1605
+ const api = this.api;
1606
+ if (!api) return;
1607
+ if (api.fail) api.fail(text);
1608
+ else api.setMessage(text);
1609
+ }
1610
+ static {
1611
+ this.ɵfac = i0.ɵɵngDeclareFactory({
1612
+ minVersion: "12.0.0",
1613
+ version: "22.2.1",
1614
+ ngImport: i0,
1615
+ type: SplashService,
1616
+ deps: [],
1617
+ target: i0.ɵɵFactoryTarget.Service
1618
+ });
1619
+ }
1620
+ static {
1621
+ this.ɵprov = i0.ɵɵngDeclareService({
1622
+ minVersion: "22.0.0",
1623
+ version: "22.2.1",
1624
+ ngImport: i0,
1625
+ type: SplashService
1626
+ });
1627
+ }
1628
+ };
1629
+ i0.ɵɵngDeclareClassMetadata({
1630
+ minVersion: "12.0.0",
1631
+ version: "22.2.1",
1632
+ ngImport: i0,
1633
+ type: SplashService,
1634
+ decorators: [{ type: Service }]
1635
+ });
1636
+ var ThemeService = class ThemeService {
1637
+ constructor() {
1638
+ this.broadcastChannel = new BroadcastChannelManager("THEME-SERVICE");
1639
+ this.broadcastMessage = "$theme-changed";
1640
+ this.prefersColorSchemeMediaQueryList = typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia("(prefers-color-scheme: dark)") : void 0;
1641
+ this.themeChanged = new BehaviorSubject("auto");
1642
+ this.renderFactory2 = inject(RendererFactory2);
1643
+ this.renderer = this.renderFactory2.createRenderer(null, null);
1644
+ this.theme = signal("light", ...ngDevMode ? [{ debugName: "theme" }] : /* istanbul ignore next */ []);
1645
+ this.auto = computed(() => this.theme() === "auto", ...ngDevMode ? [{ debugName: "auto" }] : /* istanbul ignore next */ []);
1646
+ this.themeInfo = {
1647
+ auto: {
1648
+ icon: "routine",
1649
+ name: "Tema di sistema",
1650
+ next: "light",
1651
+ tooltip: "Passa a tema chiaro"
1652
+ },
1653
+ light: {
1654
+ icon: "light_mode",
1655
+ name: "Tema chiaro",
1656
+ next: "dark",
1657
+ tooltip: "Passa a tema scuro"
1658
+ },
1659
+ dark: {
1660
+ icon: "dark_mode",
1661
+ name: "Tema scuro",
1662
+ next: "auto",
1663
+ tooltip: "Passa a tema di sistema"
1664
+ }
1665
+ };
1666
+ this.themeIcon = computed(() => this.themeInfo[this.theme()].icon, ...ngDevMode ? [{ debugName: "themeIcon" }] : /* istanbul ignore next */ []);
1667
+ this.themeName = computed(() => this.themeInfo[this.theme()].name, ...ngDevMode ? [{ debugName: "themeName" }] : /* istanbul ignore next */ []);
1668
+ this.toggleTooltip = computed(() => this.themeInfo[this.theme()].tooltip, ...ngDevMode ? [{ debugName: "toggleTooltip" }] : /* istanbul ignore next */ []);
1669
+ this.changed = this.themeChanged.asObservable();
1670
+ }
1671
+ initialize(theme = this.getPreferredTheme()) {
1672
+ if (this.prefersColorSchemeMediaQueryList) this.prefersColorSchemeMediaQueryList.onchange = () => {
1673
+ if (this.auto()) this.setTheme(this.theme());
1674
+ };
1675
+ this.broadcastChannel.subscribe({
1676
+ messageId: this.broadcastMessage,
1677
+ action: (bag) => {
1678
+ if (bag.data && this.theme() !== bag.data) this.setTheme(bag.data);
1679
+ }
1680
+ });
1681
+ this.setTheme(theme);
1682
+ }
1683
+ ngOnDestroy() {
1684
+ this.broadcastChannel.dispose();
1685
+ }
1686
+ toggleTheme() {
1687
+ this.setTheme(this.themeInfo[this.theme()].next);
1688
+ }
1689
+ getTheme() {
1690
+ const current = this.theme();
1691
+ if (current === "auto") return this.prefersColorSchemeMediaQueryList?.matches ? "dark" : "light";
1692
+ return current;
1693
+ }
1694
+ getPreferredTheme() {
1695
+ if (typeof localStorage === "undefined") return "auto";
1696
+ const stored = localStorage.getItem("preferred-theme");
1697
+ return stored && this.isTheme(stored) ? stored : "auto";
1698
+ }
1699
+ isTheme(value) {
1700
+ return value === "auto" || value === "light" || value === "dark";
1701
+ }
1702
+ setTheme(theme = "auto") {
1703
+ this.renderer.removeClass(document.body, "light");
1704
+ this.renderer.removeClass(document.body, "dark");
1705
+ this.theme.set(theme);
1706
+ if (typeof localStorage !== "undefined") localStorage.setItem("preferred-theme", theme);
1707
+ const effectiveClass = this.auto() ? this.prefersColorSchemeMediaQueryList?.matches ? "dark" : "light" : theme;
1708
+ this.renderer.addClass(document.body, effectiveClass);
1709
+ this.themeChanged.next(this.getTheme());
1710
+ this.broadcastChannel.sendMessage(this.broadcastMessage, theme);
1711
+ }
1712
+ static {
1713
+ this.ɵfac = i0.ɵɵngDeclareFactory({
1714
+ minVersion: "12.0.0",
1715
+ version: "22.2.1",
1716
+ ngImport: i0,
1717
+ type: ThemeService,
1718
+ deps: [],
1719
+ target: i0.ɵɵFactoryTarget.Service
1720
+ });
1721
+ }
1722
+ static {
1723
+ this.ɵprov = i0.ɵɵngDeclareService({
1724
+ minVersion: "22.0.0",
1725
+ version: "22.2.1",
1726
+ ngImport: i0,
1727
+ type: ThemeService
1728
+ });
1729
+ }
1730
+ };
1731
+ i0.ɵɵngDeclareClassMetadata({
1732
+ minVersion: "12.0.0",
1733
+ version: "22.2.1",
1734
+ ngImport: i0,
1735
+ type: ThemeService,
1736
+ decorators: [{ type: Service }],
1737
+ ctorParameters: () => []
1738
+ });
2375
1739
  export { AutoFocusDirective, BroadcastChannelManager, BroadcastService, CHANNEL_NAME, CopyClipboardDirective, DateFormat, DateInterval, DateIntervalChangeDirective, DeleteModel, DtoUtils, EnvironmentService, FileInfo, FormatHtmlPipe, FormatPipe, GroupModel, IDModel, ImportModel, LoginOAuthType, QueryModel, RelationModel, RemoveFocusDirective, ReplacePipe, SafeHtmlPipe, SafeUrlPipe, ScreenService, SearchCallbackPipe, SearchFilterPipe, SelectableModel, SplashService, SystemUtils, ThemeService, UpdateRelationsModel, UtilsMessages, ValueModel };
2376
- //# sourceMappingURL=arsedizioni-ars-utils-core.mjs.map
1740
+
1741
+ //# sourceMappingURL=arsedizioni-ars-utils-core.mjs.map