@arsedizioni/ars-utils 21.2.207 → 21.2.208

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,24 +1,27 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, ElementRef, Directive, input, HostListener, output, forwardRef, Pipe, EventEmitter, Injectable, signal, RendererFactory2, computed, NgModule } from '@angular/core';
2
+ import { inject, ElementRef, afterNextRender, Directive, input, DestroyRef, HostListener, output, forwardRef, Pipe, EventEmitter, signal, computed, Injectable, RendererFactory2, NgModule } from '@angular/core';
3
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
4
+ import { Subject, BehaviorSubject } from 'rxjs';
5
+ import { debounceTime } from 'rxjs/operators';
3
6
  import { HttpParams } from '@angular/common/http';
4
7
  import { parseISO, parse, format, endOfDay, getYear, getMonth, getDate, getDay, getDaysInMonth, addYears, addMonths, addDays, formatISO, isDate, isValid } from 'date-fns';
5
8
  import { it } from 'date-fns/locale';
6
9
  import { TZDate } from '@date-fns/tz';
7
- import { debounceTime } from 'rxjs/operators';
8
- import { Subject, BehaviorSubject } from 'rxjs';
9
10
  import { NG_VALIDATORS } from '@angular/forms';
10
11
  import { DomSanitizer } from '@angular/platform-browser';
11
12
  import { SelectionModel } from '@angular/cdk/collections';
12
13
  import { DateAdapter, MAT_DATE_LOCALE, MAT_DATE_FORMATS } from '@angular/material/core';
13
14
 
15
+ /**
16
+ * Directive that moves browser focus to the host element after the first render cycle.
17
+ * Apply `autoFocus` to any focusable element to set focus automatically on initialisation.
18
+ */
14
19
  class AutoFocusDirective {
15
20
  constructor() {
16
21
  this.elementRef = inject(ElementRef);
17
- }
18
- ngAfterContentInit() {
19
- setTimeout(() => {
20
- this.elementRef.nativeElement.focus();
21
- }, 500);
22
+ afterNextRender(() => {
23
+ this.elementRef.nativeElement?.focus();
24
+ });
22
25
  }
23
26
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AutoFocusDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
24
27
  static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: AutoFocusDirective, isStandalone: true, selector: "[autoFocus]", ngImport: i0 }); }
@@ -26,10 +29,10 @@ class AutoFocusDirective {
26
29
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AutoFocusDirective, decorators: [{
27
30
  type: Directive,
28
31
  args: [{
29
- selector: "[autoFocus]",
32
+ selector: '[autoFocus]',
30
33
  standalone: true
31
34
  }]
32
- }] });
35
+ }], ctorParameters: () => [] });
33
36
 
34
37
  class FileInfo {
35
38
  isValid() {
@@ -178,27 +181,25 @@ class SystemUtils {
178
181
  return nodes;
179
182
  }
180
183
  /**
181
- * Used to copare items during an array of objects sorting
182
- * @param key : the property name to sort for
183
- * @param order : the order 'asc' or 'desc'
184
- * @returns : 0 if equals, 1 if bigger, -1 if lower
184
+ * Comparator factory for sorting arrays of objects by a given property key.
185
+ * @param key - Name of the property to sort by.
186
+ * @param order - Sort direction: `'asc'` (default) or `'desc'`.
187
+ * @returns A comparator function that returns `0`, `1`, or `-1`.
185
188
  */
186
- static arraySortCompare(key, order = "asc") {
187
- const result = function innerSort(a, b) {
188
- if (!a.hasOwnProperty(key) || !b.hasOwnProperty(key)) {
189
- // property doesn't exist on either object
189
+ static arraySortCompare(key, order = 'asc') {
190
+ return function innerSort(a, b) {
191
+ if (!Object.prototype.hasOwnProperty.call(a, key) || !Object.prototype.hasOwnProperty.call(b, key)) {
190
192
  return 0;
191
193
  }
192
- const varA = typeof a[key] === "string" ? a[key].toUpperCase() : a[key];
193
- const varB = typeof b[key] === "string" ? b[key].toUpperCase() : b[key];
194
+ const varA = typeof a[key] === 'string' ? a[key].toUpperCase() : a[key];
195
+ const varB = typeof b[key] === 'string' ? b[key].toUpperCase() : b[key];
194
196
  let comparison = 0;
195
197
  if (varA > varB)
196
198
  comparison = 1;
197
199
  else if (varA < varB)
198
200
  comparison = -1;
199
- return order === "desc" ? comparison * -1 : comparison;
201
+ return order === 'desc' ? comparison * -1 : comparison;
200
202
  };
201
- return result;
202
203
  }
203
204
  /**
204
205
  * Format weight
@@ -311,21 +312,22 @@ class SystemUtils {
311
312
  return items[0];
312
313
  }
313
314
  /**
314
- * Replace stuff in text html
315
- * @param s : the string to process
316
- * @returns : the replaced string
315
+ * Wraps bare URLs in the given string with `<a>` anchor tags.
316
+ * @param s - The plain-text or HTML string to process.
317
+ * @returns The string with URLs replaced by clickable links, or `''` when `s` is falsy.
317
318
  */
318
319
  static replaceAsHtml(s) {
319
320
  if (!s)
320
321
  return '';
321
322
  const regex = /https?:\/\/.*[^\s]/ig;
322
323
  const m = regex.exec(s);
323
- if (m && m.length > 0) {
324
- for (const v of m) {
325
- s = s.replace(v, `<a href="${v}" target="_blank">${v}</a>`);
326
- }
324
+ if (!m || m.length === 0)
325
+ return s;
326
+ let result = s;
327
+ for (const v of m) {
328
+ result = result.replace(v, `<a href="${v}" target="_blank">${v}</a>`);
327
329
  }
328
- return s;
330
+ return result;
329
331
  }
330
332
  /**
331
333
  * Convert markdown to html
@@ -595,16 +597,18 @@ class SystemUtils {
595
597
  return JSON.parse(JSON.stringify(obj));
596
598
  }
597
599
  /**
598
- * Clone an object using deep cloning
599
- * @param obj : the oblect to clone
600
- * @returns : a new object
600
+ * Creates a deep clone of an object by recursively copying all own properties.
601
+ * @param obj - The source object to clone.
602
+ * @param dest - Optional pre-allocated destination object; a new instance is created when omitted.
603
+ * @returns A deep copy of `obj`.
601
604
  */
605
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
602
606
  static deepClone(obj, dest) {
603
- const cloneObj = dest ? dest : obj.constructor();
604
- const attributes = Object.keys(obj);
605
- for (const attribute of attributes) {
607
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
608
+ const cloneObj = dest ?? obj.constructor();
609
+ for (const attribute of Object.keys(obj)) {
606
610
  const property = obj[attribute];
607
- if (typeof property === "object" && property != null) {
611
+ if (typeof property === 'object' && property !== null) {
608
612
  cloneObj[attribute] = this.deepClone(property);
609
613
  }
610
614
  else {
@@ -614,22 +618,20 @@ class SystemUtils {
614
618
  return cloneObj;
615
619
  }
616
620
  /**
617
- * Check if a string is a valid UUID
618
- * @param value : the value to parse
619
- * @returns : true if is a valid UUID
621
+ * Returns `true` when `value` is a syntactically valid UUID string.
622
+ * @param value - The string to validate.
620
623
  */
621
624
  static parseUUID(value) {
622
625
  const regex = /^({|()?[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}(}|))?$/;
623
- return value?.length > 0 && regex.test(value);
626
+ return !!value && value.length > 0 && regex.test(value);
624
627
  }
625
628
  /**
626
- * Parse and ensure that UUID is not empty
627
- * @param value : the value to parse
628
- * @returns : true if valid and not empty
629
+ * Returns `true` when `value` is a valid, non-empty (non-zero) UUID.
630
+ * @param value - The string to validate.
629
631
  */
630
632
  static parseUUIDNotEmpty(value) {
631
633
  const regex = /^({|()?0{8}-(0{4}-){3}0{12}(}|))?$/;
632
- return this.parseUUID(value) && !regex.test(value);
634
+ return this.parseUUID(value) && !regex.test(value ?? '');
633
635
  }
634
636
  /**
635
637
  * Return an empty UUID
@@ -717,7 +719,7 @@ class SystemUtils {
717
719
  if (d && d.getTime() && d.getFullYear() > 1750)
718
720
  return d;
719
721
  // Parse values manually
720
- let parts = this.getDateParts(value);
722
+ const parts = this.getDateParts(value);
721
723
  if (!parts)
722
724
  return undefined;
723
725
  if (parts[0] < 100)
@@ -824,63 +826,62 @@ class SystemUtils {
824
826
  }
825
827
  }
826
828
  /**
827
- * Format number
828
- * @param value : the number to format
829
- * @param decimals : the number of decimal places
830
- * @param locale : the locale to use for formatting
831
- * @returns : the formatted number as a string
829
+ * Formats a number using `Intl.NumberFormat`.
830
+ * @param value - The number to format.
831
+ * @param decimals - Maximum decimal places (default: `2`).
832
+ * @param locale - BCP 47 locale tag (default: `'it-IT'`).
833
+ * @returns The formatted number string.
832
834
  */
833
835
  static formatNumber(value, decimals = 2, locale = 'it-IT') {
834
- return Intl.NumberFormat(locale, { style: 'decimal', minimumFractionDigits: 0, maximumFractionDigits: decimals }).format(value ?? 0);
836
+ return Intl.NumberFormat(locale, { style: 'decimal', minimumFractionDigits: 0, maximumFractionDigits: decimals }).format(value);
835
837
  }
836
838
  /**
837
- * Format currency
838
- * @param value : the number to format
839
- * @param currency : the currency code (e.g., 'EUR', 'USD')
840
- * @param decimals : the number of decimal places
841
- * @param locale : the locale to use for formatting
842
- * @returns : the formatted currency as a string
839
+ * Formats a number as a currency string using `Intl.NumberFormat`.
840
+ * @param value - The number to format.
841
+ * @param currency - ISO 4217 currency code (default: `'EUR'`).
842
+ * @param decimals - Maximum decimal places (default: `2`).
843
+ * @param locale - BCP 47 locale tag (default: `'it-IT'`).
844
+ * @returns The formatted currency string.
843
845
  */
844
846
  static formatCurrency(value, currency = 'EUR', decimals = 2, locale = 'it-IT') {
845
- return Intl.NumberFormat(locale, { style: 'currency', currency: currency, minimumFractionDigits: 0, maximumFractionDigits: decimals }).format(value ?? 0);
847
+ return Intl.NumberFormat(locale, { style: 'currency', currency, minimumFractionDigits: 0, maximumFractionDigits: decimals }).format(value);
846
848
  }
847
849
  /**
848
- * Encode url
849
- * @param value : the value to encode
850
- * @returns : the url encoded string
850
+ * Percent-encodes a string for safe inclusion in a URL.
851
+ * @param value - The string to encode.
852
+ * @returns The encoded string, or `undefined` when `value` is empty.
851
853
  */
852
854
  static urlEncode(value) {
853
- return value?.length > 0
855
+ return value.length > 0
854
856
  ? encodeURIComponent(value)
855
857
  : undefined;
856
858
  }
857
859
  /**
858
- * Decode url
859
- * @param value : the url to decode
860
- * @returns : the decode url string
860
+ * Decodes a percent-encoded URL string, treating `+` as a space.
861
+ * @param value - The encoded string to decode.
862
+ * @returns The decoded string, or `undefined` when `value` is empty or absent.
861
863
  */
862
864
  static urlDecode(value) {
863
- const regex = /\+/g;
864
- return value?.length > 0
865
- ? decodeURIComponent((value + "").replace(regex, "%20"))
865
+ return value && value.length > 0
866
+ ? decodeURIComponent(value.replace(/\+/g, '%20'))
866
867
  : undefined;
867
868
  }
868
869
  /**
869
- * Get a querystring parameter value
870
- * @param name : the parameter name
871
- * @returns : the string value
870
+ * Reads a query string parameter value from the current page URL.
871
+ * @param name - The parameter name to look up.
872
+ * @returns The decoded parameter value, or `undefined` when absent or running server-side.
872
873
  */
873
874
  static getQueryStringValueByName(name) {
874
875
  if (!this.isBrowser())
875
876
  return undefined;
876
877
  const url = window.location.href;
877
- if (url.includes("?")) {
878
- const httpParams = new HttpParams({ fromString: url.split("?")[1] });
879
- const v = decodeURIComponent(httpParams.get(name));
880
- if (v && v !== "null")
881
- return v;
882
- else
878
+ if (url.includes('?')) {
879
+ const httpParams = new HttpParams({ fromString: url.split('?')[1] });
880
+ const raw = httpParams.get(name);
881
+ if (!raw)
883
882
  return undefined;
883
+ const v = decodeURIComponent(raw);
884
+ return v && v !== 'null' ? v : undefined;
884
885
  }
885
886
  return undefined;
886
887
  }
@@ -911,7 +912,7 @@ class SystemUtils {
911
912
  * @returns the password strength info
912
913
  */
913
914
  static calculatePasswordStrength(password) {
914
- if (password?.length > 0) {
915
+ if (password && password.length > 0) {
915
916
  let score = 0;
916
917
  const suggestions = [];
917
918
  // Length
@@ -1033,22 +1034,23 @@ class SystemUtils {
1033
1034
  };
1034
1035
  const children = n.children ?? n.subFolders ?? [];
1035
1036
  if (children.length > 0) {
1036
- node.children = this._toNodes(n.children ?? n.subFolders, node);
1037
+ node.children = this._toNodes(children, node);
1037
1038
  }
1038
1039
  nodes.push(node);
1039
1040
  });
1040
1041
  return nodes;
1041
1042
  }
1042
1043
  /**
1043
- * Return the flags array to be used in selections
1044
- * @param value : the new value
1045
- * @param max: the max number of flags
1046
- */
1044
+ * Returns an array of individual power-of-2 flag values that are set in `value`.
1045
+ * @param value - The bitmask to decompose.
1046
+ * @param max - Upper-bound exponent: checks flags from `1` up to `1 << max` (default: `30`).
1047
+ * @returns Array of set flag values, or an empty array when `value` is `0`.
1048
+ */
1047
1049
  static getFlags(value, max = 30) {
1048
- if (value) {
1049
- let items = [];
1050
+ if (value !== 0) {
1051
+ const items = [];
1050
1052
  let v = 1;
1051
- let m = 1 << max;
1053
+ const m = 1 << max;
1052
1054
  while (v < m) {
1053
1055
  if ((value & v) === v)
1054
1056
  items.push(v);
@@ -1082,23 +1084,33 @@ class SystemUtils {
1082
1084
  }
1083
1085
  }
1084
1086
 
1087
+ /**
1088
+ * Directive that listens to `keyup` events on a date input and debounces changes
1089
+ * into a {@link DateInterval} model, converting shorthand strings (e.g. "d/m") to dates.
1090
+ * Apply `[dateIntervalChange]="interval"` to the host `<input>` element.
1091
+ */
1085
1092
  class DateIntervalChangeDirective {
1086
1093
  constructor() {
1094
+ /** The date interval model to update when the input value changes. */
1087
1095
  this.dateIntervalChange = input(new DateInterval(null, null), ...(ngDevMode ? [{ debugName: "dateIntervalChange" }] : /* istanbul ignore next */ []));
1096
+ /** When `true`, the directive updates the interval's end date; otherwise the start date. */
1088
1097
  this.end = input(false, ...(ngDevMode ? [{ debugName: "end" }] : /* istanbul ignore next */ []));
1089
1098
  this.subject = new Subject();
1090
- }
1091
- ngOnInit() {
1092
- this.subscription = this.subject
1093
- .pipe(debounceTime(100))
1099
+ this.destroyRef = inject(DestroyRef);
1100
+ this.subject
1101
+ .pipe(debounceTime(100), takeUntilDestroyed(this.destroyRef))
1094
1102
  .subscribe((e) => {
1095
- const value = e.target.value;
1096
- SystemUtils.changeDateInterval(value, this.dateIntervalChange(), this.end(), e.key === ' ');
1103
+ const value = e.target?.value;
1104
+ if (value !== undefined) {
1105
+ SystemUtils.changeDateInterval(value, this.dateIntervalChange(), this.end(), e.key === ' ');
1106
+ }
1097
1107
  });
1098
1108
  }
1099
- ngOnDestroy() {
1100
- this.subscription.unsubscribe();
1101
- }
1109
+ /**
1110
+ * Handles `keyup` events on the host element.
1111
+ * Prevents default browser behaviour for the space key and forwards the event to the debounce pipeline.
1112
+ * @param e - The keyboard event emitted by the host input.
1113
+ */
1102
1114
  onKeyup(e) {
1103
1115
  if (e.key === ' ') {
1104
1116
  e.preventDefault();
@@ -1115,26 +1127,36 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
1115
1127
  selector: '[dateIntervalChange]',
1116
1128
  standalone: true,
1117
1129
  }]
1118
- }], propDecorators: { dateIntervalChange: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateIntervalChange", required: false }] }], end: [{ type: i0.Input, args: [{ isSignal: true, alias: "end", required: false }] }], onKeyup: [{
1130
+ }], ctorParameters: () => [], propDecorators: { dateIntervalChange: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateIntervalChange", required: false }] }], end: [{ type: i0.Input, args: [{ isSignal: true, alias: "end", required: false }] }], onKeyup: [{
1119
1131
  type: HostListener,
1120
1132
  args: ['keyup', ['$event']]
1121
1133
  }] } });
1122
1134
 
1135
+ /**
1136
+ * Directive that copies a string payload to the clipboard when the host element is clicked.
1137
+ * Bind `[copyClipboard]="text"` to provide the content to copy and listen to `(copied)` for confirmation.
1138
+ */
1123
1139
  class CopyClipboardDirective {
1124
1140
  constructor() {
1125
- this.payload = input(undefined, { ...(ngDevMode ? { debugName: "payload" } : /* istanbul ignore next */ {}), alias: "copyClipboard" });
1141
+ /** The text to copy to the clipboard. Bound via the `copyClipboard` attribute. */
1142
+ this.payload = input(undefined, { ...(ngDevMode ? { debugName: "payload" } : /* istanbul ignore next */ {}), alias: 'copyClipboard' });
1143
+ /** Emits the copied text after a successful copy operation. */
1126
1144
  this.copied = output({ alias: 'copied' });
1127
1145
  }
1146
+ /**
1147
+ * Handles click events on the host element and copies the payload to the clipboard.
1148
+ * Emits `copied` with the copied text on success.
1149
+ * @param e - The mouse click event.
1150
+ */
1128
1151
  onClick(e) {
1129
1152
  e.preventDefault();
1130
- if (!this.payload())
1153
+ const payload = this.payload();
1154
+ if (!payload)
1131
1155
  return;
1132
1156
  if (SystemUtils.isBrowser()) {
1133
- const listener = (e) => {
1134
- const clipboard = e.clipboardData;
1135
- const payload = this.payload();
1136
- clipboard.setData('text/html', payload.toString());
1137
- e.preventDefault();
1157
+ const listener = (clipEvent) => {
1158
+ clipEvent.clipboardData?.setData('text/html', payload);
1159
+ clipEvent.preventDefault();
1138
1160
  this.copied.emit(payload);
1139
1161
  };
1140
1162
  document.addEventListener('copy', listener, false);
@@ -1156,12 +1178,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
1156
1178
  args: ['click', ['$event']]
1157
1179
  }] } });
1158
1180
 
1181
+ /**
1182
+ * Directive that delegates validation to an externally provided validator function.
1183
+ * Bind `[validator]="myFn"` where `myFn` is `(c: AbstractControl) => ValidationErrors | null`.
1184
+ */
1159
1185
  class ValidatorDirective {
1160
1186
  constructor() {
1161
- this.validator = input(null, ...(ngDevMode ? [{ debugName: "validator" }] : /* istanbul ignore next */ []));
1187
+ /** The custom validator function to apply. */
1188
+ this.validator = input(undefined, ...(ngDevMode ? [{ debugName: "validator" }] : /* istanbul ignore next */ []));
1162
1189
  }
1190
+ /**
1191
+ * Invokes the provided validator function against the given control.
1192
+ * Returns `null` (valid) when no function is bound.
1193
+ * @param control - The form control to validate.
1194
+ */
1163
1195
  validate(control) {
1164
- return this.validator()(control);
1196
+ const fn = this.validator();
1197
+ return fn ? fn(control) : null;
1165
1198
  }
1166
1199
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ValidatorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1167
1200
  static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: ValidatorDirective, isStandalone: true, selector: "[validator]", inputs: { validator: { classPropertyName: "validator", publicName: "validator", isSignal: true, isRequired: false, transformFunction: null } }, providers: [{ provide: NG_VALIDATORS, useExisting: ValidatorDirective, multi: true }], ngImport: i0 }); }
@@ -1174,27 +1207,33 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
1174
1207
  standalone: true,
1175
1208
  }]
1176
1209
  }], propDecorators: { validator: [{ type: i0.Input, args: [{ isSignal: true, alias: "validator", required: false }] }] } });
1210
+
1211
+ /**
1212
+ * Directive that validates a control using the host object's `isValid()` method
1213
+ * or a boolean expression passed via `[validIf]`.
1214
+ */
1177
1215
  class ValidIfDirective {
1178
1216
  constructor() {
1217
+ /** When `true`, the control is considered valid regardless of the bound value. */
1179
1218
  this.validIf = input(false, ...(ngDevMode ? [{ debugName: "validIf" }] : /* istanbul ignore next */ []));
1180
1219
  }
1220
+ /**
1221
+ * Validates the control value against a boolean flag or the value's own `isValid()` method.
1222
+ * @param control - The form control to validate.
1223
+ */
1181
1224
  validate(control) {
1182
1225
  let isValid = false;
1183
1226
  const c = control.value ? control.value : null;
1184
- if (!c)
1227
+ if (!c) {
1185
1228
  isValid = this.validIf() === true;
1229
+ }
1186
1230
  else {
1187
1231
  try {
1188
1232
  isValid = c.isValid();
1189
1233
  }
1190
1234
  catch { }
1191
1235
  }
1192
- if (isValid) {
1193
- return null;
1194
- }
1195
- else {
1196
- return { validIf: "Non valido." };
1197
- }
1236
+ return isValid ? null : { validIf: "Non valido." };
1198
1237
  }
1199
1238
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ValidIfDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1200
1239
  static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: ValidIfDirective, isStandalone: true, selector: "[validIf]", inputs: { validIf: { classPropertyName: "validIf", publicName: "validIf", isSignal: true, isRequired: false, transformFunction: null } }, providers: [
@@ -1219,18 +1258,26 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
1219
1258
  standalone: true,
1220
1259
  }]
1221
1260
  }], propDecorators: { validIf: [{ type: i0.Input, args: [{ isSignal: true, alias: "validIf", required: false }] }] } });
1261
+
1262
+ /**
1263
+ * Directive that validates that the host control's value equals the value of another control.
1264
+ * Bind `[equals]="otherControl"`.
1265
+ */
1222
1266
  class EqualsValidatorDirective {
1223
1267
  constructor() {
1224
- this.equals = input(null, ...(ngDevMode ? [{ debugName: "equals" }] : /* istanbul ignore next */ []));
1268
+ /** The control whose value must match the host control's value. */
1269
+ this.equals = input(undefined, ...(ngDevMode ? [{ debugName: "equals" }] : /* istanbul ignore next */ []));
1225
1270
  }
1271
+ /**
1272
+ * Validates that the host control value equals the bound control's value.
1273
+ * Returns `null` (valid) when no control is bound.
1274
+ * @param control - The form control to validate.
1275
+ */
1226
1276
  validate(control) {
1227
- const isValid = this.equals().value === control.value;
1228
- if (isValid) {
1277
+ const eq = this.equals();
1278
+ if (!eq)
1229
1279
  return null;
1230
- }
1231
- else {
1232
- return { equals: "Non valido." };
1233
- }
1280
+ return eq.value === control.value ? null : { equals: "Non valido." };
1234
1281
  }
1235
1282
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: EqualsValidatorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1236
1283
  static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: EqualsValidatorDirective, isStandalone: true, selector: "[equals]", inputs: { equals: { classPropertyName: "equals", publicName: "equals", isSignal: true, isRequired: false, transformFunction: null } }, providers: [
@@ -1255,24 +1302,36 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
1255
1302
  standalone: true,
1256
1303
  }]
1257
1304
  }], propDecorators: { equals: [{ type: i0.Input, args: [{ isSignal: true, alias: "equals", required: false }] }] } });
1305
+
1306
+ /**
1307
+ * Directive that validates that the host control's value is different from another control's value.
1308
+ * Bind `[notEqual]="otherControl"`.
1309
+ */
1258
1310
  class NotEqualValidatorDirective {
1259
1311
  constructor() {
1260
- this.notEqual = input(null, ...(ngDevMode ? [{ debugName: "notEqual" }] : /* istanbul ignore next */ []));
1312
+ /** The control whose value must differ from the host control's value. */
1313
+ this.notEqual = input(undefined, ...(ngDevMode ? [{ debugName: "notEqual" }] : /* istanbul ignore next */ []));
1261
1314
  }
1315
+ /**
1316
+ * Validates that the host control value is not equal to the bound control's value.
1317
+ * Also clears the `notequal` error on the other control when the host becomes valid.
1318
+ * Returns `null` (valid) when no control is bound.
1319
+ * @param control - The form control to validate.
1320
+ */
1262
1321
  validate(control) {
1263
1322
  const notEqual = this.notEqual();
1323
+ if (!notEqual)
1324
+ return null;
1264
1325
  const isValid = (!notEqual.value && !control.value) || (notEqual.value !== control.value);
1265
- const errors = isValid ? null : { 'notequal': true };
1266
- // immediate show up
1267
- const notEqualValue = this.notEqual();
1326
+ const errors = isValid ? null : { notequal: true };
1268
1327
  if (errors) {
1269
1328
  control.markAsTouched();
1270
1329
  }
1271
- else if (notEqualValue.hasError('notequal')) {
1272
- notEqualValue.setErrors({ 'notequal': null });
1273
- notEqualValue.updateValueAndValidity({ onlySelf: true, emitEvent: false });
1274
- notEqualValue.markAsTouched();
1275
- notEqualValue.markAsDirty();
1330
+ else if (notEqual.hasError('notequal')) {
1331
+ notEqual.setErrors({ notequal: null });
1332
+ notEqual.updateValueAndValidity({ onlySelf: true, emitEvent: false });
1333
+ notEqual.markAsTouched();
1334
+ notEqual.markAsDirty();
1276
1335
  }
1277
1336
  return errors;
1278
1337
  }
@@ -1299,27 +1358,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
1299
1358
  standalone: true,
1300
1359
  }]
1301
1360
  }], propDecorators: { notEqual: [{ type: i0.Input, args: [{ isSignal: true, alias: "notEqual", required: false }] }] } });
1361
+
1362
+ /**
1363
+ * Directive that validates a semicolon-separated list of email addresses.
1364
+ * Apply `emails` to a text input containing one or more addresses separated by `;`.
1365
+ */
1302
1366
  class EmailsValidatorDirective {
1367
+ /**
1368
+ * Validates each address in a semicolon-separated email list.
1369
+ * Returns `null` when the control is empty.
1370
+ * @param control - The form control to validate.
1371
+ */
1303
1372
  validate(control) {
1304
- let isValid = true;
1305
1373
  const input = control.value;
1306
1374
  if (!input || input.length === 0)
1307
1375
  return null;
1308
1376
  const parts = input.replaceAll(/\r\n/g, '').split(';');
1309
- for (const part of parts) {
1310
- if (part.length > 0) {
1311
- if (!SystemUtils.parseEmail(part)) {
1312
- isValid = false;
1313
- break;
1314
- }
1315
- }
1316
- }
1317
- if (!isValid) {
1318
- return { emails: "Elenco non valido." };
1319
- }
1320
- else {
1321
- return null;
1322
- }
1377
+ const isValid = parts.every(part => part.length === 0 || !!SystemUtils.parseEmail(part));
1378
+ return isValid ? null : { emails: "Elenco non valido." };
1323
1379
  }
1324
1380
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: EmailsValidatorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1325
1381
  static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: EmailsValidatorDirective, isStandalone: true, selector: "[emails]", providers: [
@@ -1344,18 +1400,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
1344
1400
  standalone: true,
1345
1401
  }]
1346
1402
  }] });
1403
+
1404
+ /**
1405
+ * Directive that validates a control value as a GUID / UUID string.
1406
+ * Apply `guid` to a text input that expects a valid UUID.
1407
+ */
1347
1408
  class GuidValidatorDirective {
1409
+ /**
1410
+ * Validates that the control value is a well-formed GUID / UUID.
1411
+ * Returns `null` when the control is empty.
1412
+ * @param control - The form control to validate.
1413
+ */
1348
1414
  validate(control) {
1349
1415
  const input = control.value;
1350
1416
  if (!input || input.length === 0)
1351
1417
  return null;
1352
- const isValid = SystemUtils.parseUUID(input);
1353
- if (!isValid) {
1354
- return { guid: "Non valido." };
1355
- }
1356
- else {
1357
- return null;
1358
- }
1418
+ return SystemUtils.parseUUID(input) ? null : { guid: "Non valido." };
1359
1419
  }
1360
1420
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: GuidValidatorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1361
1421
  static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: GuidValidatorDirective, isStandalone: true, selector: "[guid]", providers: [
@@ -1380,19 +1440,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
1380
1440
  standalone: true,
1381
1441
  }]
1382
1442
  }] });
1443
+
1444
+ /**
1445
+ * Directive that validates a control value as a parseable SQL-compatible date string.
1446
+ * Apply `sqlDate` to a text input that expects a date after year 1750.
1447
+ */
1383
1448
  class SqlDateValidatorDirective {
1449
+ /**
1450
+ * Validates that the control value can be parsed as a date after 1750.
1451
+ * Returns `null` when the control is empty.
1452
+ * @param control - The form control to validate.
1453
+ */
1384
1454
  validate(control) {
1385
1455
  const input = control.value;
1386
1456
  if (!input || input.length === 0)
1387
1457
  return null;
1388
1458
  const d = endOfDay(SystemUtils.parseDate(input));
1389
- const isValid = d.getFullYear() > 1750;
1390
- if (!isValid) {
1391
- return { sqlDate: "Non valido." };
1392
- }
1393
- else {
1394
- return null;
1395
- }
1459
+ return d.getFullYear() > 1750 ? null : { sqlDate: "Non valido." };
1396
1460
  }
1397
1461
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: SqlDateValidatorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1398
1462
  static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: SqlDateValidatorDirective, isStandalone: true, selector: "[sqlDate]", providers: [
@@ -1417,20 +1481,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
1417
1481
  standalone: true,
1418
1482
  }]
1419
1483
  }] });
1484
+
1485
+ /**
1486
+ * Directive that validates that a control value is not a future date.
1487
+ * Apply `notFuture` to a text input that expects a date on or before today.
1488
+ */
1420
1489
  class NotFutureValidatorDirective {
1490
+ /**
1491
+ * Validates that the control value represents a date that is not in the future.
1492
+ * Returns `null` when the control is empty.
1493
+ * @param control - The form control to validate.
1494
+ */
1421
1495
  validate(control) {
1422
1496
  const input = control.value;
1423
1497
  if (!input || input.length === 0)
1424
1498
  return null;
1425
1499
  const today = endOfDay(new Date());
1426
1500
  const d = endOfDay(SystemUtils.parseDate(input));
1427
- const isValid = d <= today;
1428
- if (!isValid) {
1429
- return { notFuture: "Non valido." };
1430
- }
1431
- else {
1432
- return null;
1433
- }
1501
+ return d <= today ? null : { notFuture: "Non valido." };
1434
1502
  }
1435
1503
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: NotFutureValidatorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1436
1504
  static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: NotFutureValidatorDirective, isStandalone: true, selector: "[notFuture]", providers: [
@@ -1455,16 +1523,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
1455
1523
  standalone: true,
1456
1524
  }]
1457
1525
  }] });
1526
+
1527
+ /**
1528
+ * Directive that validates a control value as a well-formed URL.
1529
+ * Apply `url` to a text input that expects a URL.
1530
+ */
1458
1531
  class UrlValidatorDirective {
1532
+ /**
1533
+ * Validates that the control value is a well-formed URL.
1534
+ * Returns `null` (valid) when the control is empty.
1535
+ * @param control - The form control to validate.
1536
+ */
1459
1537
  validate(control) {
1460
1538
  const input = control.value;
1461
- const isValid = SystemUtils.parseUrl(input);
1462
- if (!isValid) {
1463
- return { url: "Non valido." };
1464
- }
1465
- else {
1539
+ if (!input || input.length === 0)
1466
1540
  return null;
1467
- }
1541
+ return SystemUtils.parseUrl(input) ? null : { url: "Non valido." };
1468
1542
  }
1469
1543
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: UrlValidatorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1470
1544
  static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: UrlValidatorDirective, isStandalone: true, selector: "[url]", providers: [
@@ -1489,25 +1563,32 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
1489
1563
  standalone: true,
1490
1564
  }]
1491
1565
  }] });
1566
+
1567
+ /**
1568
+ * Directive that validates a file size against configurable minimum and maximum bounds.
1569
+ * Bind `[fileSize]` together with `[size]="fileSizeInMb"`, `[maxSizeMb]`, and `[minSizeMb]`.
1570
+ */
1492
1571
  class FileSizeValidatorDirective {
1493
1572
  constructor() {
1573
+ /** Maximum allowed file size in megabytes. Defaults to 5. */
1494
1574
  this.maxSizeMb = input(5, ...(ngDevMode ? [{ debugName: "maxSizeMb" }] : /* istanbul ignore next */ []));
1575
+ /** Minimum required file size in megabytes. Defaults to 0. */
1495
1576
  this.minSizeMb = input(0, ...(ngDevMode ? [{ debugName: "minSizeMb" }] : /* istanbul ignore next */ []));
1496
- this.size = input(null, ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
1577
+ /** The actual file size in megabytes to validate against the bounds. */
1578
+ this.size = input(undefined, ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
1497
1579
  }
1580
+ /**
1581
+ * Validates that the bound file size falls within the configured min/max range.
1582
+ * Returns `null` when no control value is present.
1583
+ * @param control - The form control to validate.
1584
+ */
1498
1585
  validate(control) {
1499
1586
  const input = control.value;
1500
- const size = this.size();
1501
- const s = size ?? 0;
1502
1587
  if (!input)
1503
1588
  return null;
1589
+ const s = this.size() ?? 0;
1504
1590
  const isValid = s <= this.maxSizeMb() && s >= this.minSizeMb();
1505
- if (isValid) {
1506
- return null;
1507
- }
1508
- else {
1509
- return { fileSize: "Non valido." };
1510
- }
1591
+ return isValid ? null : { fileSize: "Non valido." };
1511
1592
  }
1512
1593
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: FileSizeValidatorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1513
1594
  static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: FileSizeValidatorDirective, isStandalone: true, selector: "[fileSize]", inputs: { maxSizeMb: { classPropertyName: "maxSizeMb", publicName: "maxSizeMb", isSignal: true, isRequired: false, transformFunction: null }, minSizeMb: { classPropertyName: "minSizeMb", publicName: "minSizeMb", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, providers: [
@@ -1532,22 +1613,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
1532
1613
  standalone: true,
1533
1614
  }]
1534
1615
  }], propDecorators: { maxSizeMb: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxSizeMb", required: false }] }], minSizeMb: [{ type: i0.Input, args: [{ isSignal: true, alias: "minSizeMb", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
1616
+
1617
+ /**
1618
+ * Directive that validates that a control value does not exceed a maximum word count.
1619
+ * Bind `[maxTerms]="10"` to allow at most 10 whitespace-separated terms.
1620
+ */
1535
1621
  class MaxTermsValidatorDirective {
1536
1622
  constructor() {
1623
+ /** The maximum number of whitespace-separated terms allowed. */
1537
1624
  this.maxTerms = input(0, ...(ngDevMode ? [{ debugName: "maxTerms" }] : /* istanbul ignore next */ []));
1538
1625
  }
1626
+ /**
1627
+ * Validates that the control value contains no more than the configured number of terms.
1628
+ * Returns `null` when the control is empty.
1629
+ * @param control - The form control to validate.
1630
+ */
1539
1631
  validate(control) {
1540
1632
  const input = control.value;
1541
1633
  if (!input)
1542
1634
  return null;
1543
- const terms = input?.match(/\S+/g)?.length ?? 0;
1544
- const isValid = terms <= this.maxTerms();
1545
- if (isValid) {
1546
- return null;
1547
- }
1548
- else {
1549
- return { maxTerms: "Non valido." };
1550
- }
1635
+ const terms = input.match(/\S+/g)?.length ?? 0;
1636
+ return terms <= this.maxTerms() ? null : { maxTerms: "Non valido." };
1551
1637
  }
1552
1638
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: MaxTermsValidatorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1553
1639
  static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: MaxTermsValidatorDirective, isStandalone: true, selector: "[maxTerms]", inputs: { maxTerms: { classPropertyName: "maxTerms", publicName: "maxTerms", isSignal: true, isRequired: false, transformFunction: null } }, providers: [
@@ -1572,16 +1658,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
1572
1658
  standalone: true,
1573
1659
  }]
1574
1660
  }], propDecorators: { maxTerms: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxTerms", required: false }] }] } });
1661
+
1662
+ /**
1663
+ * Directive that validates a control value as a sufficiently strong password.
1664
+ * Apply `password` to a password input.
1665
+ */
1575
1666
  class PasswordValidatorDirective {
1667
+ /**
1668
+ * Validates that the control value meets the minimum password-strength requirements.
1669
+ * @param control - The form control to validate.
1670
+ */
1576
1671
  validate(control) {
1577
1672
  const input = control.value ?? '';
1578
1673
  const strength = SystemUtils.calculatePasswordStrength(input);
1579
- if (!strength.isValid) {
1580
- return { password: "Non valido." };
1581
- }
1582
- else {
1583
- return null;
1584
- }
1674
+ return strength.isValid ? null : { password: "Non valido." };
1585
1675
  }
1586
1676
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: PasswordValidatorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1587
1677
  static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: PasswordValidatorDirective, isStandalone: true, selector: "[password]", providers: [
@@ -1606,14 +1696,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
1606
1696
  standalone: true,
1607
1697
  }]
1608
1698
  }] });
1699
+
1700
+ /**
1701
+ * Directive that validates a time string against optional allowed time slot ranges.
1702
+ * Bind `[time]` and optionally `[slots]="'08:00-12:00|14:00-18:00'"` (pipe-separated ranges).
1703
+ */
1609
1704
  class TimeValidatorDirective {
1610
1705
  constructor() {
1611
- this.slots = input(null, ...(ngDevMode ? [{ debugName: "slots" }] : /* istanbul ignore next */ []));
1706
+ /** Optional pipe-separated list of allowed time ranges, e.g. `"08:00-12:00|14:00-18:00"`. */
1707
+ this.slots = input(undefined, ...(ngDevMode ? [{ debugName: "slots" }] : /* istanbul ignore next */ []));
1612
1708
  }
1613
1709
  /**
1614
- * Retrieve hours as a number from a time string (e: 12:00)
1615
- * @param value : the time string
1616
- * @returns the hours as a number or -1
1710
+ * Parses a `"HH:MM"` time string into a comparable integer (e.g. `"09:30"` -> `930`).
1711
+ * Returns `-1` when the string is not a valid time.
1712
+ * @param value - The time string to parse.
1617
1713
  */
1618
1714
  getTime(value) {
1619
1715
  const p = value.split(':');
@@ -1623,37 +1719,33 @@ class TimeValidatorDirective {
1623
1719
  if (hh < 0 || hh > 23)
1624
1720
  return -1;
1625
1721
  const mm = parseInt(p[1]);
1626
- if (mm < 0 || hh > 59)
1722
+ if (mm < 0 || mm > 59)
1627
1723
  return -1;
1628
1724
  return parseInt(p[0] + p[1]);
1629
1725
  }
1726
+ /**
1727
+ * Validates that the control value is a valid time string and, when slots are configured,
1728
+ * that it falls within at least one of the allowed ranges.
1729
+ * Returns `null` when the control is empty.
1730
+ * @param control - The form control to validate.
1731
+ */
1630
1732
  validate(control) {
1631
1733
  const input = control.value;
1632
1734
  if (!input || input.length === 0)
1633
1735
  return null;
1634
- let isValid = false;
1635
1736
  const t = this.getTime(input);
1636
- if (t !== -1) {
1637
- const slotsValue = this.slots();
1638
- if (slotsValue) {
1639
- const slots = slotsValue.split('|');
1640
- slots.forEach(s => {
1641
- const t1 = this.getTime(s.substring(0, 5));
1642
- const t2 = this.getTime(s.substring(6));
1643
- if (t1 !== -1 && t2 !== -1 && t1 <= t && t2 >= t) {
1644
- // Good
1645
- isValid = true;
1646
- return;
1647
- }
1648
- });
1649
- }
1650
- }
1651
- if (!isValid) {
1737
+ if (t === -1)
1652
1738
  return { time: "Non valido." };
1739
+ const slotsValue = this.slots();
1740
+ if (slotsValue) {
1741
+ const isValid = slotsValue.split('|').some(s => {
1742
+ const t1 = this.getTime(s.substring(0, 5));
1743
+ const t2 = this.getTime(s.substring(6));
1744
+ return t1 !== -1 && t2 !== -1 && t1 <= t && t2 >= t;
1745
+ });
1746
+ return isValid ? null : { time: "Non valido." };
1653
1747
  }
1654
- else {
1655
- return null;
1656
- }
1748
+ return null;
1657
1749
  }
1658
1750
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: TimeValidatorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1659
1751
  static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: TimeValidatorDirective, isStandalone: true, selector: "[time]", inputs: { slots: { classPropertyName: "slots", publicName: "slots", isSignal: true, isRequired: false, transformFunction: null } }, providers: [
@@ -1678,18 +1770,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
1678
1770
  standalone: true,
1679
1771
  }]
1680
1772
  }], propDecorators: { slots: [{ type: i0.Input, args: [{ isSignal: true, alias: "slots", required: false }] }] } });
1773
+
1774
+ /**
1775
+ * Directive that validates that a string control value is not blank (whitespace-only).
1776
+ * Apply `notEmpty` to a text input where non-blank content is required.
1777
+ */
1681
1778
  class NotEmptyValidatorDirective {
1779
+ /**
1780
+ * Validates that the control value is a non-blank string.
1781
+ * Returns `null` when the control is empty or not a string.
1782
+ * @param control - The form control to validate.
1783
+ */
1682
1784
  validate(control) {
1683
1785
  const input = control?.value;
1684
1786
  if (!input || typeof input !== 'string' || input.length === 0)
1685
1787
  return null;
1686
- const isValid = input.trim().length > 0;
1687
- if (!isValid) {
1688
- return { notEmpty: true };
1689
- }
1690
- else {
1691
- return null;
1692
- }
1788
+ return input.trim().length > 0 ? null : { notEmpty: true };
1693
1789
  }
1694
1790
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: NotEmptyValidatorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1695
1791
  static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: NotEmptyValidatorDirective, isStandalone: true, selector: "[notEmpty]", providers: [
@@ -1715,29 +1811,47 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
1715
1811
  }]
1716
1812
  }] });
1717
1813
 
1814
+ /**
1815
+ * General-purpose formatting pipe that converts a raw value to a locale-aware string
1816
+ * based on the specified format type.
1817
+ *
1818
+ * Supported types: `'date'` / `'D'`, `'currency'` / `'C'`, `'number'` / `'N'`,
1819
+ * `'number0'` / `'N0'`, `'percentage'` / `'P'`.
1820
+ *
1821
+ * Usage: `{{ value | format:'currency' }}`
1822
+ */
1718
1823
  class FormatPipe {
1719
- transform(value, type = 'date', pattern = "dd/MM/yyyy") {
1824
+ /**
1825
+ * Formats a value according to the specified type and optional pattern.
1826
+ * Returns `undefined` when the value is `null` or `undefined`, or when the type is unrecognised.
1827
+ * @param value - The raw value to format.
1828
+ * @param type - The format type identifier (default: `'date'`).
1829
+ * @param pattern - The date pattern used when `type` is `'date'` (default: `'dd/MM/yyyy'`).
1830
+ * @returns A formatted string, or `undefined` when the value cannot be formatted.
1831
+ */
1832
+ transform(value, type = 'date', pattern = 'dd/MM/yyyy') {
1720
1833
  if (value === undefined || value === null)
1721
1834
  return undefined;
1722
1835
  switch (type) {
1723
1836
  case 'D':
1724
- case 'date':
1837
+ case 'date': {
1725
1838
  const d = SystemUtils.parseDate(value, it);
1726
1839
  if (d)
1727
1840
  return format(d, pattern, { locale: it });
1728
1841
  break;
1842
+ }
1729
1843
  case 'C':
1730
1844
  case 'currency':
1731
- return new Intl.NumberFormat('it-IT', { style: 'currency', currency: 'EUR' }).format(value ?? 0);
1845
+ return new Intl.NumberFormat('it-IT', { style: 'currency', currency: 'EUR' }).format(value);
1732
1846
  case 'N':
1733
1847
  case 'number':
1734
- return new Intl.NumberFormat('it-IT', { style: 'decimal', minimumFractionDigits: 0, maximumFractionDigits: 2 }).format(value ?? 0);
1848
+ return new Intl.NumberFormat('it-IT', { style: 'decimal', minimumFractionDigits: 0, maximumFractionDigits: 2 }).format(value);
1735
1849
  case 'N0':
1736
1850
  case 'number0':
1737
- return new Intl.NumberFormat('it-IT', { style: 'decimal', minimumFractionDigits: 0, maximumFractionDigits: 0 }).format(value ?? 0);
1851
+ return new Intl.NumberFormat('it-IT', { style: 'decimal', minimumFractionDigits: 0, maximumFractionDigits: 0 }).format(value);
1738
1852
  case 'P':
1739
1853
  case 'percentage':
1740
- return new Intl.NumberFormat('it-IT', { style: 'percent', minimumFractionDigits: 0, maximumFractionDigits: 0 }).format(value ?? 0);
1854
+ return new Intl.NumberFormat('it-IT', { style: 'percent', minimumFractionDigits: 0, maximumFractionDigits: 0 }).format(value);
1741
1855
  }
1742
1856
  return undefined;
1743
1857
  }
@@ -1752,17 +1866,30 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
1752
1866
  }]
1753
1867
  }] });
1754
1868
 
1869
+ /**
1870
+ * Pipe that applies a global regex replacement on a string and returns the result
1871
+ * as sanitized HTML. When `regexValue` is `'\n'` and no `replaceValue` is given,
1872
+ * newlines are replaced with `<br>` tags.
1873
+ *
1874
+ * Usage: `{{ text | replace:'\n':'' }}`
1875
+ */
1755
1876
  class ReplacePipe {
1756
1877
  constructor() {
1757
1878
  this.sanitizer = inject(DomSanitizer);
1758
1879
  }
1880
+ /**
1881
+ * Replaces all occurrences of `regexValue` in `value` with `replaceValue`.
1882
+ * Returns `undefined` when `value` is empty or `undefined`.
1883
+ * @param value - The source string to process.
1884
+ * @param regexValue - The regex pattern string to match (applied with the global flag).
1885
+ * @param replaceValue - The replacement string. Defaults to `'<br>'` when `regexValue` is `'\n'` and this is falsy.
1886
+ * @returns A `SafeHtml` value with all matches replaced, or `undefined` when the input is empty.
1887
+ */
1759
1888
  transform(value, regexValue, replaceValue) {
1760
1889
  if (!value)
1761
1890
  return undefined;
1762
- if (regexValue === '\n' && !replaceValue) {
1763
- replaceValue = '<br>';
1764
- }
1765
- return this.sanitizer.bypassSecurityTrustHtml(value.replace(new RegExp(regexValue, 'g'), replaceValue));
1891
+ const replacement = (regexValue === '\n' && !replaceValue) ? '<br>' : replaceValue;
1892
+ return this.sanitizer.bypassSecurityTrustHtml(value.replace(new RegExp(regexValue, 'g'), replacement));
1766
1893
  }
1767
1894
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ReplacePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
1768
1895
  static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.2.9", ngImport: i0, type: ReplacePipe, isStandalone: true, name: "replace" }); }
@@ -1775,10 +1902,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
1775
1902
  }]
1776
1903
  }] });
1777
1904
 
1905
+ /**
1906
+ * Pipe that marks an HTML string as trusted so Angular does not escape it when
1907
+ * bound via `[innerHTML]`.
1908
+ *
1909
+ * Usage: `<div [innerHTML]="html | safeHtml"></div>`
1910
+ */
1778
1911
  class SafeHtmlPipe {
1779
1912
  constructor() {
1780
1913
  this.sanitizer = inject(DomSanitizer);
1781
1914
  }
1915
+ /**
1916
+ * Bypasses Angular's HTML sanitization and returns a `SafeHtml` instance.
1917
+ * @param value - The raw HTML string to trust. Treated as an empty string when `undefined`.
1918
+ * @returns A `SafeHtml` value that can be bound to `[innerHTML]` without escaping.
1919
+ */
1782
1920
  transform(value) {
1783
1921
  return this.sanitizer.bypassSecurityTrustHtml(value ?? '');
1784
1922
  }
@@ -1793,10 +1931,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
1793
1931
  }]
1794
1932
  }] });
1795
1933
 
1934
+ /**
1935
+ * Pipe that marks a URL string as a trusted resource URL so Angular does not block it
1936
+ * when bound to attributes such as `[src]` or `[href]` on iframes, objects, or embeds.
1937
+ *
1938
+ * Usage: `<iframe [src]="url | safeUrl"></iframe>`
1939
+ */
1796
1940
  class SafeUrlPipe {
1797
1941
  constructor() {
1798
1942
  this.sanitizer = inject(DomSanitizer);
1799
1943
  }
1944
+ /**
1945
+ * Bypasses Angular's resource-URL sanitization and returns a `SafeResourceUrl` instance.
1946
+ * @param value - The URL string to trust. Treated as an empty string when `undefined`.
1947
+ * @returns A `SafeResourceUrl` that can be bound to resource URL attributes without blocking.
1948
+ */
1800
1949
  transform(value) {
1801
1950
  return this.sanitizer.bypassSecurityTrustResourceUrl(value ?? '');
1802
1951
  }
@@ -1811,11 +1960,25 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
1811
1960
  }]
1812
1961
  }] });
1813
1962
 
1963
+ /**
1964
+ * Impure pipe that filters an array using a caller-provided predicate function.
1965
+ * Because the pipe is impure it re-evaluates on every change-detection cycle,
1966
+ * which is necessary when the predicate's captured state changes.
1967
+ *
1968
+ * Usage: `*ngFor="let item of items | callback:myFilter"`
1969
+ */
1814
1970
  class SearchCallbackPipe {
1971
+ /**
1972
+ * Filters `items` by applying `callback` to each element.
1973
+ * Returns the original array unchanged when either argument is falsy.
1974
+ * @param items - The source array to filter. May be `undefined`.
1975
+ * @param callback - A predicate function that returns `true` for items to keep.
1976
+ * @returns A new filtered array, the original array when no callback is provided,
1977
+ * or `undefined` when `items` is `undefined`.
1978
+ */
1815
1979
  transform(items, callback) {
1816
- if (!items || !callback) {
1980
+ if (!items || !callback)
1817
1981
  return items;
1818
- }
1819
1982
  return items.filter(item => callback(item));
1820
1983
  }
1821
1984
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: SearchCallbackPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
@@ -1830,35 +1993,41 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
1830
1993
  }]
1831
1994
  }] });
1832
1995
 
1833
- // ---------------------------------------------------------------------------------
1834
- // 2023.0921 aggiunto un campo opzionale di tipo SearchFilterMetadata per i conteggi
1835
- //
1836
- // - nel chiamante:
1837
- // let items: Searchable[] = [...];
1838
- // let myFilterText: string = 'testo del filtro';
1839
- // const myFilterMetadata: SearchFilterMetadata = { total: 0, count: 0 };
1840
- //
1841
- // <div *ngFor="let item of items | search:myFilterText:myFilterMetadata">
1842
- // <div>Totale: {{ myFilterMetadata.total }}, Filtrati: {{ myFilterMetadata.count }}</div>
1843
- // ---------------------------------------------------------------------------------
1996
+ /**
1997
+ * Impure pipe that filters an array of searchable items against a text query.
1998
+ *
1999
+ * Each item is matched either via its `searchBag.name` property (when present)
2000
+ * or by converting the item itself to a lowercase string. The optional `metadata`
2001
+ * argument is updated in-place with the total item count and the filtered count,
2002
+ * making it usable in the template alongside `*ngFor`.
2003
+ *
2004
+ * Usage:
2005
+ * ```html
2006
+ * <div *ngFor="let item of items | search:filterText:meta">...</div>
2007
+ * <div>Showing {{ meta.count }} of {{ meta.total }}</div>
2008
+ * ```
2009
+ */
1844
2010
  class SearchFilterPipe {
2011
+ /**
2012
+ * Filters `items` by performing a case-insensitive substring match against `value`.
2013
+ * When `items` or `value` is falsy the original array is returned unfiltered.
2014
+ * @param items - The source array to filter. May be `undefined`.
2015
+ * @param value - The search text to match against each item. May be `undefined`.
2016
+ * @param metadata - Optional object that is updated with `total` and `count` after filtering.
2017
+ * @returns The filtered array, the original array when no filter text is given,
2018
+ * or `undefined` when `items` is `undefined`.
2019
+ */
1845
2020
  transform(items, value, metadata) {
1846
2021
  metadata ??= { total: 0, count: 0 };
1847
- if (!items || !value) {
2022
+ if (!items || !value)
1848
2023
  return items;
1849
- }
1850
- let result = items.filter(item => {
1851
- if (item) {
1852
- if (item.searchBag)
1853
- return item.searchBag.name
1854
- .toLowerCase()
1855
- .indexOf(value.toLowerCase()) !== -1;
1856
- else
1857
- return item.toLowerCase().indexOf(value.toLowerCase()) !== -1;
1858
- }
1859
- return null;
2024
+ const query = value.toLowerCase();
2025
+ const result = items.filter(item => {
2026
+ if (!item)
2027
+ return false;
2028
+ const text = item.searchBag?.name ?? (typeof item === 'string' ? item : undefined);
2029
+ return text?.toLowerCase().includes(query) ?? false;
1860
2030
  });
1861
- // aggiorna i conteggi
1862
2031
  metadata.total = items.length;
1863
2032
  metadata.count = result.length;
1864
2033
  return result;
@@ -1874,10 +2043,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
1874
2043
  }]
1875
2044
  }] });
1876
2045
 
2046
+ /**
2047
+ * Pipe that converts plain-text newlines (`\r\n`, `\r`, `\n`) to HTML `<br>` tags
2048
+ * and marks the result as trusted HTML so Angular does not escape it.
2049
+ *
2050
+ * Usage: `{{ text | formatHtml }}`
2051
+ */
1877
2052
  class FormatHtmlPipe {
1878
2053
  constructor() {
1879
2054
  this.sanitizer = inject(DomSanitizer);
1880
2055
  }
2056
+ /**
2057
+ * Transforms a plain-text string into sanitized HTML by replacing newline characters
2058
+ * with `<br>` tags.
2059
+ * @param value - The input string to transform. Treated as an empty string when `undefined`.
2060
+ * @returns A `SafeHtml` value that can be rendered with `[innerHTML]`.
2061
+ */
1881
2062
  transform(value) {
1882
2063
  return this.sanitizer.bypassSecurityTrustHtml((value ?? '').replaceAll(/(?:\r\n|\r|\n)/g, '<br>'));
1883
2064
  }
@@ -1910,131 +2091,148 @@ const UtilsMessages = {
1910
2091
  UTILS_DIALOGS_SELECT_OPTIONS_CHANGED: '§utils-dialogs-select-options-changed'
1911
2092
  };
1912
2093
 
2094
+ /**
2095
+ * Generic selection model that tracks a set of selected items identified by a lookup field.
2096
+ * Wraps Angular CDK's `SelectionModel` and adds lookup-based add/remove logic.
2097
+ *
2098
+ * @typeParam T - The item type held in the selection.
2099
+ * @typeParam V - The type of the lookup key used to identify items.
2100
+ */
1913
2101
  class SelectableModel {
2102
+ /**
2103
+ * Snapshot of all items currently tracked by this model (selected or previously toggled).
2104
+ * Backed by a signal — reads are always up to date.
2105
+ */
1914
2106
  get all() {
1915
- return this._all;
2107
+ return this._all();
1916
2108
  }
2109
+ /** The underlying CDK `SelectionModel` (provides `.selected`, `.isSelected`, etc.). */
1917
2110
  get current() {
1918
2111
  return this._current;
1919
2112
  }
1920
- constructor(allowMultiSelect = true, lookupFieldName = "id") {
2113
+ /**
2114
+ * @param allowMultiSelect - When `true` (default), multiple items can be selected simultaneously.
2115
+ * @param lookupFieldName - Name of the property used as the unique key when searching the internal list (default: `'id'`).
2116
+ */
2117
+ constructor(allowMultiSelect = true, lookupFieldName = 'id') {
2118
+ /**
2119
+ * Emits whenever the selection changes.
2120
+ * Carries the affected item, or `undefined` when the whole selection is cleared.
2121
+ */
1921
2122
  this.changed = new EventEmitter();
1922
- this._all = [];
2123
+ this._all = signal([], ...(ngDevMode ? [{ debugName: "_all" }] : /* istanbul ignore next */ []));
2124
+ /** Signal that is `true` when at least one item is selected. */
2125
+ this.hasValue = computed(() => this._all().length > 0, ...(ngDevMode ? [{ debugName: "hasValue" }] : /* istanbul ignore next */ []));
1923
2126
  this._current = new SelectionModel(allowMultiSelect, []);
1924
- this._all = [];
1925
2127
  this._lookupFieldName = lookupFieldName;
1926
2128
  }
1927
2129
  /**
1928
- * Update the view of an item
1929
- * @param item : the item to update
1930
- * @param lookupValue : the value
2130
+ * Toggles the CDK selection state of an item that already exists in the tracked list.
2131
+ * Has no effect when `lookupValue` does not match any tracked item.
2132
+ * @param item - The item whose selection state should be toggled.
2133
+ * @param lookupValue - The key value used to locate the item in the internal list.
1931
2134
  */
1932
2135
  updateCurrent(item, lookupValue) {
1933
- const p = SystemUtils.arrayFindIndexByKey(this._all, this._lookupFieldName, lookupValue);
2136
+ const p = SystemUtils.arrayFindIndexByKey(this._all(), this._lookupFieldName, lookupValue);
1934
2137
  if (p !== -1) {
1935
2138
  this._current.toggle(item);
1936
2139
  this.changed.emit(item);
1937
2140
  }
1938
2141
  }
1939
2142
  /**
1940
- * Select an item
1941
- * @param item : the item to update
1942
- * @param lookupValue : the value
2143
+ * Toggles an item in both the internal list and the CDK selection.
2144
+ * Adds the item when it is not yet tracked; removes it when it is.
2145
+ * @param item - The item to toggle.
2146
+ * @param lookupValue - The key value used to locate or register the item.
1943
2147
  */
1944
2148
  toggle(item, lookupValue) {
1945
- if (lookupValue) {
1946
- const p = SystemUtils.arrayFindIndexByKey(this._all, this._lookupFieldName, lookupValue);
1947
- if (p === -1) {
1948
- this._all.push(item);
1949
- }
1950
- else {
1951
- this._all.splice(p, 1);
1952
- }
1953
- this._current.toggle(item);
1954
- this.changed.emit(item);
2149
+ if (lookupValue === undefined)
2150
+ return;
2151
+ const p = SystemUtils.arrayFindIndexByKey(this._all(), this._lookupFieldName, lookupValue);
2152
+ if (p === -1) {
2153
+ this._all.update(arr => [...arr, item]);
2154
+ }
2155
+ else {
2156
+ this._all.update(arr => arr.filter((_, i) => i !== p));
1955
2157
  }
2158
+ this._current.toggle(item);
2159
+ this.changed.emit(item);
1956
2160
  }
1957
2161
  /**
1958
- * Select an item
1959
- * @param item : the item to update
1960
- * @param lookupValue : the value
2162
+ * Adds an item to the internal list (when not already present) and marks it as selected.
2163
+ * @param item - The item to select.
2164
+ * @param lookupValue - The key value used to locate or register the item.
1961
2165
  */
1962
2166
  select(item, lookupValue) {
1963
- if (lookupValue) {
1964
- const p = SystemUtils.arrayFindIndexByKey(this._all, this._lookupFieldName, lookupValue);
1965
- if (p === -1) {
1966
- this._all.push(item);
1967
- }
1968
- this._current.select(item);
1969
- this.changed.emit(item);
2167
+ if (lookupValue === undefined)
2168
+ return;
2169
+ const p = SystemUtils.arrayFindIndexByKey(this._all(), this._lookupFieldName, lookupValue);
2170
+ if (p === -1) {
2171
+ this._all.update(arr => [...arr, item]);
1970
2172
  }
2173
+ this._current.select(item);
2174
+ this.changed.emit(item);
1971
2175
  }
1972
2176
  /**
1973
- * Deselect an item
1974
- * @param item : the item to update
1975
- * @param lookupValue : the value
2177
+ * Removes an item from the internal list and deselects it in the CDK model.
2178
+ * Has no effect when the item is not currently tracked.
2179
+ * @param item - The item to deselect.
2180
+ * @param lookupValue - The key value used to locate the item in the internal list.
1976
2181
  */
1977
2182
  deselect(item, lookupValue) {
1978
- if (lookupValue) {
1979
- const p = SystemUtils.arrayFindIndexByKey(this._all, this._lookupFieldName, lookupValue);
1980
- if (p !== -1) {
1981
- this._all.splice(p, 1);
1982
- this._current.deselect(item);
1983
- this.changed.emit(item);
1984
- }
2183
+ if (lookupValue === undefined)
2184
+ return;
2185
+ const p = SystemUtils.arrayFindIndexByKey(this._all(), this._lookupFieldName, lookupValue);
2186
+ if (p !== -1) {
2187
+ this._all.update(arr => arr.filter((_, i) => i !== p));
2188
+ this._current.deselect(item);
2189
+ this.changed.emit(item);
1985
2190
  }
1986
2191
  }
1987
2192
  /**
1988
- * Deselect all specified item lookup values
1989
- * @param lookupValues : the values
2193
+ * Deselects all items whose lookup key is contained in `lookupValues`.
2194
+ * @param lookupValues - Array of key values identifying the items to deselect.
1990
2195
  */
1991
2196
  deselectByValues(lookupValues) {
1992
- if (!lookupValues || lookupValues.length === 0)
2197
+ if (lookupValues.length === 0)
1993
2198
  return;
1994
- lookupValues.forEach(n => {
1995
- const p = SystemUtils.arrayFindIndexByKey(this._all, this._lookupFieldName, n);
2199
+ for (const key of lookupValues) {
2200
+ const p = SystemUtils.arrayFindIndexByKey(this._all(), this._lookupFieldName, key);
1996
2201
  if (p !== -1) {
1997
- const item = this._all[p];
1998
- this._all.splice(p, 1);
2202
+ const item = this._all()[p];
2203
+ this._all.update(arr => arr.filter((_, i) => i !== p));
1999
2204
  this._current.deselect(item);
2000
2205
  this.changed.emit(item);
2001
2206
  }
2002
- });
2207
+ }
2003
2208
  }
2004
2209
  /**
2005
- * Clear current view
2006
- * @param clearFunc : the function used to clear
2210
+ * Runs `clearFunc` against every currently selected item, then clears the CDK selection.
2211
+ * The internal tracked list is **not** affected; only the CDK selection is cleared.
2212
+ * @param clearFunc - Callback invoked for each currently selected item before clearing.
2007
2213
  */
2008
2214
  clearCurrent(clearFunc) {
2009
- this._current.selected.forEach(n => {
2010
- clearFunc(n);
2011
- });
2215
+ for (const item of this._current.selected) {
2216
+ clearFunc(item);
2217
+ }
2012
2218
  this._current.clear();
2013
- this.changed.emit();
2219
+ this.changed.emit(undefined);
2014
2220
  }
2015
2221
  /**
2016
- * Deselect all items
2222
+ * Clears both the internal tracked list and the CDK selection.
2017
2223
  */
2018
2224
  clear() {
2019
- this._all = [];
2225
+ this._all.set([]);
2020
2226
  this._current.clear();
2021
- this.changed.emit();
2227
+ this.changed.emit(undefined);
2022
2228
  }
2023
2229
  /**
2024
- * Checks if an item is selected using its lookup value
2025
- * @param lookupValue : the value
2026
- * @returns : true if the value is selected
2230
+ * Returns `true` when the item identified by `lookupValue` is present in the tracked list.
2231
+ * @param lookupValue - The key value to look up.
2027
2232
  */
2028
2233
  isSelected(lookupValue) {
2029
2234
  return (lookupValue !== undefined &&
2030
- SystemUtils.arrayFindIndexByKey(this._all, this._lookupFieldName, lookupValue) !== -1);
2031
- }
2032
- /**
2033
- * Checks if a selection exists
2034
- * @returns : true if at least one selection exists
2035
- */
2036
- hasValue() {
2037
- return this._all.length > 0;
2235
+ SystemUtils.arrayFindIndexByKey(this._all(), this._lookupFieldName, lookupValue) !== -1);
2038
2236
  }
2039
2237
  }
2040
2238
 
@@ -2043,37 +2241,48 @@ class SelectableModel {
2043
2241
  * https://developer.mozilla.org/en-US/docs/Web/API/BroadcastChannel
2044
2242
  */
2045
2243
  /**
2046
- * Web Standard Broadcast Messages Manager
2047
- * https://developer.mozilla.org/en-US/docs/Web/API/BroadcastChannel
2244
+ * Wrapper around the native Web `BroadcastChannel` API that adds typed messaging,
2245
+ * multiple named subscriptions, and a graceful disposal mechanism.
2246
+ *
2247
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/BroadcastChannel
2048
2248
  */
2049
2249
  class BroadcastChannelManager {
2050
2250
  /**
2051
- * Get the current channel registration
2251
+ * The name of the underlying `BroadcastChannel`, or `undefined` when the API
2252
+ * is not supported by the current browser.
2052
2253
  */
2053
2254
  get currentBus() {
2054
2255
  return this.channel?.name;
2055
2256
  }
2056
- constructor(bus = "ARS-CHANNEL") {
2257
+ /**
2258
+ * Opens a `BroadcastChannel` with the given name (when the API is available).
2259
+ * @param bus - The channel name to open (default: `'ARS-CHANNEL'`).
2260
+ */
2261
+ constructor(bus = 'ARS-CHANNEL') {
2057
2262
  this.subject = new Subject();
2058
2263
  this.subscriptions = [];
2059
2264
  if ('BroadcastChannel' in window) {
2060
2265
  this.channel = new BroadcastChannel(bus);
2061
- this.channel.onmessageerror = (_e) => {
2062
- console.error('[BroadcastChannelManager] Errore di ricezione del messaggio', _e);
2266
+ this.channel.onmessageerror = (e) => {
2267
+ console.error('[BroadcastChannelManager] Message receive error', e);
2063
2268
  };
2064
2269
  this.channel.onmessage = (e) => {
2065
- this.subject.next(e.data);
2066
2270
  const bag = e.data;
2067
- if (bag) {
2068
- const bagInfo = this.subscriptions.find(m => m.messageId === bag.messageId);
2069
- bagInfo?.action(bag);
2070
- }
2271
+ if (!bag)
2272
+ return;
2273
+ this.subject.next(bag);
2274
+ const info = this.subscriptions.find(m => m.messageId === bag.messageId);
2275
+ info?.action(bag);
2071
2276
  };
2072
2277
  }
2073
2278
  else {
2074
- console.error('[BroadcastChannelManager] Non supportato');
2279
+ console.error('[BroadcastChannelManager] BroadcastChannel API is not supported in this browser');
2075
2280
  }
2076
2281
  }
2282
+ /**
2283
+ * Closes the underlying channel and completes the internal subject after a short delay
2284
+ * to allow any in-flight messages to be delivered.
2285
+ */
2077
2286
  dispose() {
2078
2287
  setTimeout(() => {
2079
2288
  this.subscriptions = [];
@@ -2082,22 +2291,22 @@ class BroadcastChannelManager {
2082
2291
  this.subject.complete();
2083
2292
  }, 1000);
2084
2293
  }
2085
- /**
2086
- * overloads
2087
- */
2088
- sendMessage(messageIdorBag, dataOrDelay, delay) {
2294
+ /** @internal Implementation that handles all overloads. */
2295
+ sendMessage(messageIdOrBag, dataOrDelay, delay) {
2296
+ const effectiveDelay = (typeof dataOrDelay === 'number' ? dataOrDelay : delay) ?? 0;
2089
2297
  setTimeout(() => {
2090
- if (typeof messageIdorBag === 'string') {
2091
- this.channel?.postMessage({ messageId: messageIdorBag, data: dataOrDelay });
2298
+ if (typeof messageIdOrBag === 'string') {
2299
+ this.channel?.postMessage({ messageId: messageIdOrBag, data: dataOrDelay });
2092
2300
  }
2093
2301
  else {
2094
- this.channel?.postMessage(messageIdorBag);
2302
+ this.channel?.postMessage(messageIdOrBag);
2095
2303
  }
2096
- }, (typeof dataOrDelay === 'number' ? dataOrDelay : delay) ?? 0);
2304
+ }, effectiveDelay);
2097
2305
  }
2098
2306
  /**
2099
- * Subscribe to a message
2100
- * @param args The subscription info, see also {@link BroadcastChannelSubscriberInfo}
2307
+ * Registers a handler for messages with the given `messageId`.
2308
+ * If a handler for the same `messageId` is already registered it is replaced.
2309
+ * @param args - The subscription descriptor containing the message ID and handler.
2101
2310
  */
2102
2311
  subscribe(args) {
2103
2312
  const i = this.subscriptions.findIndex(m => m.messageId === args.messageId);
@@ -2109,8 +2318,8 @@ class BroadcastChannelManager {
2109
2318
  }
2110
2319
  }
2111
2320
  /**
2112
- * Remove a message subscription
2113
- * @param messageId The message id
2321
+ * Removes the subscription for the given message ID, if one exists.
2322
+ * @param messageId - The message type identifier whose subscription should be removed.
2114
2323
  */
2115
2324
  unsubscribe(messageId) {
2116
2325
  const i = this.subscriptions.findIndex(m => m.messageId === messageId);
@@ -2119,60 +2328,66 @@ class BroadcastChannelManager {
2119
2328
  }
2120
2329
  }
2121
2330
  /**
2122
- * Remove all the current subscriptions
2331
+ * Removes all registered subscriptions.
2123
2332
  */
2124
2333
  unsubscribeAll() {
2125
2334
  this.subscriptions = [];
2126
2335
  }
2127
2336
  }
2128
2337
 
2338
+ /**
2339
+ * Application-level messaging service that combines two transports:
2340
+ * - An in-process RxJS `Subject` for same-tab communication (`sendMessage` / `getMessage`).
2341
+ * - A native `BroadcastChannel` for cross-tab communication (`sendChannelMessage` / `subscribeChannelMessage`).
2342
+ */
2129
2343
  class BroadcastService {
2130
2344
  constructor() {
2131
2345
  this.subject = new Subject();
2132
2346
  this.channel = new BroadcastChannelManager('ARS-CHANNEL');
2133
2347
  }
2134
2348
  /**
2135
- * Get a new instance of broadcast channel
2349
+ * Creates a new standalone `BroadcastChannelManager` instance bound to the shared ARS channel.
2350
+ * Useful when a caller needs direct channel access outside the service.
2351
+ * @returns A new `BroadcastChannelManager` connected to `'ARS-CHANNEL'`.
2136
2352
  */
2137
2353
  static GetChannel() {
2138
- return new BroadcastChannelManager("ARS-CHANNEL");
2354
+ return new BroadcastChannelManager('ARS-CHANNEL');
2139
2355
  }
2140
2356
  ngOnDestroy() {
2141
2357
  this.channel?.dispose();
2142
2358
  }
2143
2359
  /**
2144
- * Send a message
2145
- * @param id : the message id
2146
- * @param data : the data to send
2147
- * @param delay : the number of milliseconds to wait before sending the message
2360
+ * Publishes a message to the in-process subject, optionally after a delay.
2361
+ * @param id - The message type identifier.
2362
+ * @param data - Optional payload to attach to the message.
2363
+ * @param delay - Milliseconds to wait before publishing (default: `0`).
2148
2364
  */
2149
- sendMessage(id, data = undefined, delay = 0) {
2365
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2366
+ sendMessage(id, data, delay = 0) {
2150
2367
  setTimeout(() => {
2151
2368
  this.subject.next({ id, data });
2152
2369
  }, delay);
2153
2370
  }
2154
2371
  /**
2155
- * Send a channel message
2156
- * @param id : the message bag
2157
- * @param data : the data bag
2158
- * @param delay : the number of milliseconds to wait before sending the message
2159
- */
2372
+ * Publishes a typed message over the native `BroadcastChannel` (cross-tab), optionally after a delay.
2373
+ * @param id - The message type identifier.
2374
+ * @param data - Optional typed payload to send.
2375
+ * @param delay - Milliseconds to wait before sending (forwarded to the channel manager).
2376
+ */
2160
2377
  sendChannelMessage(id, data, delay) {
2161
- setTimeout(() => {
2162
- this.channel.sendMessage(id, data, delay);
2163
- }, delay);
2378
+ this.channel.sendMessage(id, data, delay);
2164
2379
  }
2165
2380
  /**
2166
- * Subscribe channel message
2167
- * @param id : the message id
2168
- * @param action : the actionto perform
2381
+ * Registers a handler that is invoked whenever a channel message with the given `id` is received.
2382
+ * @param id - The message type identifier to listen for.
2383
+ * @param action - Callback invoked with the full `BroadcastChannelMessageBag` when a matching message arrives.
2169
2384
  */
2170
2385
  subscribeChannelMessage(id, action) {
2171
- this.channel.subscribe({ messageId: id, action: action });
2386
+ this.channel.subscribe({ messageId: id, action });
2172
2387
  }
2173
2388
  /**
2174
- * Get the next message as observable
2175
- * @returns : the observable object
2389
+ * Returns an `Observable` that emits every in-process message published via `sendMessage`.
2390
+ * @returns An observable stream of `BroadcastMessageInfo` objects.
2176
2391
  */
2177
2392
  getMessage() {
2178
2393
  return this.subject.asObservable();
@@ -2187,37 +2402,54 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
2187
2402
  }]
2188
2403
  }] });
2189
2404
 
2405
+ /**
2406
+ * Application-wide environment configuration service.
2407
+ * Exposes writable signals for the base URIs used throughout the application.
2408
+ * Values should be set once during application bootstrap (e.g. in `APP_INITIALIZER`).
2409
+ */
2190
2410
  class EnvironmentService {
2191
2411
  constructor() {
2192
- this._appUri = '';
2193
- this._appServiceUri = '';
2194
- this._appLoginRedirectUri = '';
2195
- this._appServiceLoginUri = '';
2196
- }
2197
- get appUri() {
2198
- return this._appUri;
2199
- }
2200
- set appUri(value) {
2201
- this._appUri = value;
2202
- }
2203
- get appServiceUri() {
2204
- return this._appServiceUri;
2205
- }
2206
- set appServiceUri(value) {
2207
- this._appServiceUri = value;
2208
- }
2209
- get appLoginRedirectUri() {
2210
- return this._appLoginRedirectUri ?? this._appUri;
2211
- }
2212
- set appLoginRedirectUri(value) {
2213
- this._appLoginRedirectUri = value;
2214
- }
2215
- get appServiceLoginUri() {
2216
- return this._appServiceLoginUri ?? this._appServiceUri;
2217
- }
2218
- set appServiceLoginUri(value) {
2219
- this._appServiceLoginUri = value;
2220
- }
2412
+ /** Writable signal holding the base URI of the frontend application (e.g. `https://app.example.com`). */
2413
+ this.appUriSignal = signal('', ...(ngDevMode ? [{ debugName: "appUriSignal" }] : /* istanbul ignore next */ []));
2414
+ /** Writable signal holding the base URI of the backend API service (e.g. `https://api.example.com`). */
2415
+ this.appServiceUriSignal = signal('', ...(ngDevMode ? [{ debugName: "appServiceUriSignal" }] : /* istanbul ignore next */ []));
2416
+ /**
2417
+ * Writable signal holding the redirect URI used after login.
2418
+ * When empty, `appLoginRedirectUri` falls back to `appUri`.
2419
+ */
2420
+ this.appLoginRedirectUriSignal = signal('', ...(ngDevMode ? [{ debugName: "appLoginRedirectUriSignal" }] : /* istanbul ignore next */ []));
2421
+ /**
2422
+ * Writable signal holding the login endpoint URI of the backend service.
2423
+ * When empty, `appServiceLoginUri` falls back to `appServiceUri`.
2424
+ */
2425
+ this.appServiceLoginUriSignal = signal('', ...(ngDevMode ? [{ debugName: "appServiceLoginUriSignal" }] : /* istanbul ignore next */ []));
2426
+ // ── Computed: fallback logic ──────────────────────────────────────────────
2427
+ this._effectiveLoginRedirectUri = computed(() => this.appLoginRedirectUriSignal() || this.appUriSignal(), ...(ngDevMode ? [{ debugName: "_effectiveLoginRedirectUri" }] : /* istanbul ignore next */ []));
2428
+ this._effectiveServiceLoginUri = computed(() => this.appServiceLoginUriSignal() || this.appServiceUriSignal(), ...(ngDevMode ? [{ debugName: "_effectiveServiceLoginUri" }] : /* istanbul ignore next */ []));
2429
+ }
2430
+ // ── Getter / setter API (kept for backward compatibility) ─────────────────
2431
+ /** Base URI of the frontend application. */
2432
+ get appUri() { return this.appUriSignal(); }
2433
+ /** @param value - The base URI of the frontend application. */
2434
+ set appUri(value) { this.appUriSignal.set(value); }
2435
+ /** Base URI of the backend API service. */
2436
+ get appServiceUri() { return this.appServiceUriSignal(); }
2437
+ /** @param value - The base URI of the backend API service. */
2438
+ set appServiceUri(value) { this.appServiceUriSignal.set(value); }
2439
+ /**
2440
+ * Effective login redirect URI.
2441
+ * Returns the explicitly set value, or falls back to `appUri` when empty.
2442
+ */
2443
+ get appLoginRedirectUri() { return this._effectiveLoginRedirectUri(); }
2444
+ /** @param value - The redirect URI to use after login. */
2445
+ set appLoginRedirectUri(value) { this.appLoginRedirectUriSignal.set(value); }
2446
+ /**
2447
+ * Effective service login URI.
2448
+ * Returns the explicitly set value, or falls back to `appServiceUri` when empty.
2449
+ */
2450
+ get appServiceLoginUri() { return this._effectiveServiceLoginUri(); }
2451
+ /** @param value - The login endpoint URI of the backend service. */
2452
+ set appServiceLoginUri(value) { this.appServiceLoginUriSignal.set(value); }
2221
2453
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: EnvironmentService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
2222
2454
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: EnvironmentService, providedIn: 'root' }); }
2223
2455
  }
@@ -2228,13 +2460,29 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
2228
2460
  }]
2229
2461
  }] });
2230
2462
 
2463
+ /**
2464
+ * Service that exposes reactive screen and device-capability information.
2465
+ */
2231
2466
  class ScreenService {
2232
2467
  constructor() {
2468
+ /**
2469
+ * Writable signal that holds the current Material breakpoint alias
2470
+ * (e.g. `'xs'`, `'sm'`, `'md'`, `'lg'`, `'xl'`).
2471
+ * Updated externally by the breakpoint observer in the consuming component or module.
2472
+ */
2233
2473
  this.mq = signal('', ...(ngDevMode ? [{ debugName: "mq" }] : /* istanbul ignore next */ []));
2234
2474
  }
2475
+ /**
2476
+ * Returns `true` when the primary input mechanism can hover over elements
2477
+ * with coarse pointer precision (i.e. a touch screen).
2478
+ */
2235
2479
  get isTouchable() {
2236
2480
  return SystemUtils.isTouchable();
2237
2481
  }
2482
+ /**
2483
+ * Returns `true` when the current browser is Internet Explorer or the legacy Edge (EdgeHTML).
2484
+ * Modern Chromium-based Edge reports a different user-agent and will return `false`.
2485
+ */
2238
2486
  get isIEOrEdge() {
2239
2487
  return /msie\s|trident\/|edge\//i.test(window.navigator.userAgent);
2240
2488
  }
@@ -2248,124 +2496,115 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
2248
2496
  }]
2249
2497
  }] });
2250
2498
 
2499
+ /**
2500
+ * Application-level theme management service.
2501
+ *
2502
+ * Responsibilities:
2503
+ * - Persists the user's preferred theme in `localStorage`.
2504
+ * - Applies `'light'` or `'dark'` CSS class to `<body>`.
2505
+ * - Reacts to OS-level colour-scheme changes when the theme is set to `'auto'`.
2506
+ * - Synchronises theme changes across browser tabs via `BroadcastChannel`.
2507
+ * - Exposes reactive signals (`themeIcon`, `themeName`, `toggleTooltip`) for the UI.
2508
+ */
2251
2509
  class ThemeService {
2252
2510
  constructor() {
2253
- // messaggi broadcast
2254
2511
  this.broadcastChannel = new BroadcastChannelManager('THEME-SERVICE');
2255
2512
  this.broadcastMessage = '$theme-changed';
2256
- // per intercettare il cambio di tema a livello di sistema
2257
2513
  this.prefersColorSchemeMediaQueryList = window.matchMedia('(prefers-color-scheme: dark)');
2258
- /**
2259
- * Theme changed e.
2260
- * Notify the consuming component about the incoming theme change.
2261
- * @example
2262
- * // React to theme changes
2263
- * this.themeService.themeChanged
2264
- * .pipe(takeUntil(this.unsubscribe))
2265
- * .subscribe((value: ThemeType) => {
2266
- * // do the job done....
2267
- * });
2268
- */
2269
2514
  this.themeChanged = new BehaviorSubject('auto');
2270
- // espone al consumatore un'observable
2271
- this.changed = this.themeChanged.asObservable();
2272
2515
  this.renderFactory2 = inject(RendererFactory2);
2273
2516
  this.renderer = this.renderFactory2.createRenderer(null, null);
2274
- /**
2275
- * Current theme
2276
- */
2277
- this.theme = signal("light", ...(ngDevMode ? [{ debugName: "theme" }] : /* istanbul ignore next */ []));
2278
- /**
2279
- * The current theme is "System default"
2280
- */
2517
+ /** The currently selected `ThemeType` (may be `'auto'`). */
2518
+ this.theme = signal('light', ...(ngDevMode ? [{ debugName: "theme" }] : /* istanbul ignore next */ []));
2519
+ /** `true` when the theme is set to follow the system preference. */
2281
2520
  this.auto = computed(() => this.theme() === 'auto', ...(ngDevMode ? [{ debugName: "auto" }] : /* istanbul ignore next */ []));
2282
- /**
2283
- * Configuration schema about the icon, the text and the toggle() sequence
2284
- */
2285
2521
  this.themeInfo = {
2286
- ['auto']: { icon: 'routine', name: 'Tema di sistema', next: 'light', tooltip: 'Passa a tema chiaro', },
2287
- ['light']: { icon: 'light_mode', name: 'Tema chiaro', next: 'dark', tooltip: 'Passa a tema scuro', },
2288
- ['dark']: { icon: 'dark_mode', name: 'Tema scuro', next: 'auto', tooltip: 'Passa a tema di sistema', },
2522
+ auto: { icon: 'routine', name: 'Tema di sistema', next: 'light', tooltip: 'Passa a tema chiaro' },
2523
+ light: { icon: 'light_mode', name: 'Tema chiaro', next: 'dark', tooltip: 'Passa a tema scuro' },
2524
+ dark: { icon: 'dark_mode', name: 'Tema scuro', next: 'auto', tooltip: 'Passa a tema di sistema' },
2289
2525
  };
2290
- /**
2291
- * Icon of the current theme (mat-icon code)
2292
- */
2526
+ /** Reactive Material icon code for the current theme (use with `<mat-icon>`). */
2293
2527
  this.themeIcon = computed(() => this.themeInfo[this.theme()].icon, ...(ngDevMode ? [{ debugName: "themeIcon" }] : /* istanbul ignore next */ []));
2294
- /**
2295
- * Current theme name
2296
- */
2528
+ /** Reactive human-readable name of the current theme. */
2297
2529
  this.themeName = computed(() => this.themeInfo[this.theme()].name, ...(ngDevMode ? [{ debugName: "themeName" }] : /* istanbul ignore next */ []));
2298
- /**
2299
- * Tooltip text on toggle
2300
- */
2530
+ /** Reactive tooltip text for the theme toggle button. */
2301
2531
  this.toggleTooltip = computed(() => this.themeInfo[this.theme()].tooltip, ...(ngDevMode ? [{ debugName: "toggleTooltip" }] : /* istanbul ignore next */ []));
2532
+ this.changed = this.themeChanged.asObservable();
2302
2533
  }
2303
2534
  /**
2304
- * Initialize
2535
+ * Initialises the service: loads the preferred theme, registers OS-change listener,
2536
+ * and subscribes to cross-tab broadcast messages.
2537
+ * Call this once during application bootstrap (e.g. in `APP_INITIALIZER`).
2538
+ * @param theme - The initial theme to apply (defaults to the value stored in `localStorage`).
2305
2539
  */
2306
2540
  initialize(theme = this.getPreferredTheme()) {
2307
- // React to current theme
2308
- this.prefersColorSchemeMediaQueryList.onchange = (_) => this.auto() ? this.setTheme(theme) : null;
2309
- // Support boradcast channel messaging
2541
+ this.prefersColorSchemeMediaQueryList.onchange = () => {
2542
+ if (this.auto())
2543
+ this.setTheme(this.theme());
2544
+ };
2310
2545
  this.broadcastChannel.subscribe({
2311
2546
  messageId: this.broadcastMessage,
2312
2547
  action: (bag) => {
2313
- if (this.theme() !== bag.data) {
2548
+ if (bag.data && this.theme() !== bag.data) {
2314
2549
  this.setTheme(bag.data);
2315
2550
  }
2316
2551
  }
2317
2552
  });
2318
- // Load theme
2319
2553
  this.setTheme(theme);
2320
2554
  }
2321
2555
  ngOnDestroy() {
2322
2556
  this.broadcastChannel.dispose();
2323
2557
  }
2324
2558
  /**
2325
- * Get the current preferred theme
2559
+ * Advances the theme through the cycle: `auto` → `light` → `dark` → `auto`.
2326
2560
  */
2327
- getPreferredTheme() {
2328
- let theme = localStorage.getItem('preferred-theme');
2329
- if (!this.isTheme(theme)) {
2330
- theme = 'auto';
2561
+ toggleTheme() {
2562
+ this.setTheme(this.themeInfo[this.theme()].next);
2563
+ }
2564
+ /**
2565
+ * Returns the resolved effective theme (`'light'` or `'dark'`), taking the OS preference
2566
+ * into account when the current mode is `'auto'`.
2567
+ * @returns `'light'` or `'dark'` — never `'auto'`.
2568
+ */
2569
+ getTheme() {
2570
+ if (this.auto()) {
2571
+ return this.prefersColorSchemeMediaQueryList.matches ? 'dark' : 'light';
2331
2572
  }
2332
- return theme;
2573
+ return this.getPreferredTheme();
2333
2574
  }
2334
2575
  /**
2335
- * Check if a value string is a theme type
2336
- * @param value : th string value
2337
- * @returns true if the the value is a valid theme type
2576
+ * Reads the theme stored in `localStorage` and validates it.
2577
+ * Falls back to `'auto'` when the stored value is missing or invalid.
2578
+ * @returns The persisted `ThemeType`, or `'auto'` as default.
2579
+ */
2580
+ getPreferredTheme() {
2581
+ const stored = localStorage.getItem('preferred-theme');
2582
+ return stored && this.isTheme(stored) ? stored : 'auto';
2583
+ }
2584
+ /**
2585
+ * Returns `true` when `value` is a valid `ThemeType` string.
2586
+ * Acts as a TypeScript type guard.
2587
+ * @param value - The string to check.
2338
2588
  */
2339
2589
  isTheme(value) {
2340
- return ['auto', 'light', 'dark'].includes(value);
2590
+ return value === 'auto' || value === 'light' || value === 'dark';
2341
2591
  }
2342
2592
  /**
2343
- * Set the new theme
2344
- * @param theme the new theme
2593
+ * Applies the given theme to `<body>` (CSS class), persists it to `localStorage`,
2594
+ * notifies the `changed` observable, and broadcasts the change to other tabs.
2595
+ * @param theme - The `ThemeType` to apply (defaults to `'auto'`).
2345
2596
  */
2346
2597
  setTheme(theme = 'auto') {
2347
2598
  this.renderer.removeClass(document.body, 'light');
2348
2599
  this.renderer.removeClass(document.body, 'dark');
2349
2600
  this.theme.set(theme);
2350
- localStorage.setItem("preferred-theme", this.theme());
2351
- this.renderer.addClass(document.body, this.auto() ? (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light") : theme);
2352
- // Notify that current theme has changed
2601
+ localStorage.setItem('preferred-theme', theme);
2602
+ const effectiveClass = this.auto()
2603
+ ? (this.prefersColorSchemeMediaQueryList.matches ? 'dark' : 'light')
2604
+ : theme;
2605
+ this.renderer.addClass(document.body, effectiveClass);
2353
2606
  this.themeChanged.next(this.getTheme());
2354
- this.broadcastChannel.sendMessage(this.broadcastMessage, this.theme());
2355
- }
2356
- /**
2357
- * Toggle theme
2358
- */
2359
- toggleTheme() {
2360
- this.setTheme(this.themeInfo[this.theme()].next);
2361
- }
2362
- /**
2363
- * Get current real theme: light or dark
2364
- */
2365
- getTheme() {
2366
- return (this.auto()
2367
- ? (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light")
2368
- : this.getPreferredTheme());
2607
+ this.broadcastChannel.sendMessage(this.broadcastMessage, theme);
2369
2608
  }
2370
2609
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ThemeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
2371
2610
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ThemeService, providedIn: "root" }); }
@@ -2375,7 +2614,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
2375
2614
  args: [{
2376
2615
  providedIn: "root",
2377
2616
  }]
2378
- }] });
2617
+ }], ctorParameters: () => [] });
2379
2618
 
2380
2619
  class ArsCoreModule {
2381
2620
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ArsCoreModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
@@ -2495,7 +2734,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
2495
2734
  }]
2496
2735
  }] });
2497
2736
 
2498
- /** Creates an array and fills it with values. */
2737
+ /**
2738
+ * Creates an array of the given length, filling each slot with the result of `valueFunction`.
2739
+ * @param length - Number of elements to create.
2740
+ * @param valueFunction - Factory called with each index to produce the element value.
2741
+ * @returns Typed array of `length` elements.
2742
+ */
2499
2743
  function range(length, valueFunction) {
2500
2744
  const valuesArray = Array(length);
2501
2745
  for (let i = 0; i < length; i++) {
@@ -2526,32 +2770,61 @@ const MAT_DATE_FNS_FORMATS = {
2526
2770
  monthYearA11yLabel: 'LLLL uuuu',
2527
2771
  },
2528
2772
  };
2529
- /** Adds date-fns support to Angular Material. */
2773
+ /**
2774
+ * date-fns adapter that integrates Angular Material's date picker with the date-fns library,
2775
+ * applying `Europe/Rome` timezone for all parsed and created dates.
2776
+ */
2530
2777
  class DateFnsAdapter extends DateAdapter {
2531
2778
  constructor() {
2532
2779
  super();
2533
2780
  const matDateLocale = inject(MAT_DATE_LOCALE, { optional: true });
2534
- this.setLocale(matDateLocale);
2781
+ if (matDateLocale) {
2782
+ this.setLocale(matDateLocale);
2783
+ }
2535
2784
  }
2785
+ /**
2786
+ * Returns the year component of the given date.
2787
+ * @param date - The source date.
2788
+ */
2536
2789
  getYear(date) {
2537
2790
  return getYear(date);
2538
2791
  }
2792
+ /**
2793
+ * Returns the zero-based month index of the given date (0 = January).
2794
+ * @param date - The source date.
2795
+ */
2539
2796
  getMonth(date) {
2540
2797
  return getMonth(date);
2541
2798
  }
2799
+ /**
2800
+ * Returns the day-of-month of the given date (1-based).
2801
+ * @param date - The source date.
2802
+ */
2542
2803
  getDate(date) {
2543
2804
  return getDate(date);
2544
2805
  }
2806
+ /**
2807
+ * Returns the day-of-week of the given date (0 = Sunday).
2808
+ * @param date - The source date.
2809
+ */
2545
2810
  getDayOfWeek(date) {
2546
2811
  return getDay(date);
2547
2812
  }
2813
+ /**
2814
+ * Returns an array of 12 month name strings formatted for the active locale.
2815
+ * @param style - One of `'long'`, `'short'`, or `'narrow'`.
2816
+ */
2548
2817
  getMonthNames(style) {
2549
2818
  const pattern = MONTH_FORMATS[style];
2550
2819
  return range(12, i => this.format(new Date(2017, i, 1), pattern));
2551
2820
  }
2821
+ /**
2822
+ * Returns an array of 31 day-of-month label strings formatted using `Intl.DateTimeFormat`
2823
+ * when available, falling back to plain numeric strings.
2824
+ */
2552
2825
  getDateNames() {
2553
2826
  const dtf = typeof Intl !== 'undefined'
2554
- ? new Intl.DateTimeFormat(this.locale.code, {
2827
+ ? new Intl.DateTimeFormat(this.locale?.code, {
2555
2828
  day: 'numeric',
2556
2829
  timeZone: 'utc',
2557
2830
  })
@@ -2565,28 +2838,52 @@ class DateFnsAdapter extends DateAdapter {
2565
2838
  date.setUTCHours(0, 0, 0, 0);
2566
2839
  return dtf.format(date).replace(/[\u200e\u200f]/g, '');
2567
2840
  }
2568
- return i + '';
2841
+ return String(i + 1);
2569
2842
  });
2570
2843
  }
2844
+ /**
2845
+ * Returns an array of 7 day-of-week name strings formatted for the active locale.
2846
+ * @param style - One of `'long'`, `'short'`, or `'narrow'`.
2847
+ */
2571
2848
  getDayOfWeekNames(style) {
2572
2849
  const pattern = DAY_OF_WEEK_FORMATS[style];
2573
2850
  return range(7, i => this.format(new Date(2017, 0, i + 1), pattern));
2574
2851
  }
2852
+ /**
2853
+ * Returns the four-digit year string for the given date.
2854
+ * @param date - The source date.
2855
+ */
2575
2856
  getYearName(date) {
2576
2857
  return this.format(date, 'y');
2577
2858
  }
2859
+ /**
2860
+ * Returns the first day of the week for the active locale (0 = Sunday, 1 = Monday, …).
2861
+ */
2578
2862
  getFirstDayOfWeek() {
2579
- return this.locale.options?.weekStartsOn ?? 0;
2863
+ return this.locale?.options?.weekStartsOn ?? 0;
2580
2864
  }
2865
+ /**
2866
+ * Returns the number of days in the month of the given date.
2867
+ * @param date - The source date.
2868
+ */
2581
2869
  getNumDaysInMonth(date) {
2582
2870
  return getDaysInMonth(date);
2583
2871
  }
2872
+ /**
2873
+ * Creates an independent copy of the given date.
2874
+ * @param date - The date to clone.
2875
+ */
2584
2876
  clone(date) {
2585
2877
  return new Date(date.getTime());
2586
2878
  }
2879
+ /**
2880
+ * Creates a `Date` in the `Europe/Rome` timezone for the given year, month, and day.
2881
+ * Throws an `Error` when any component is out of range.
2882
+ * @param year - Full four-digit year.
2883
+ * @param month - Zero-based month index (0 = January, 11 = December).
2884
+ * @param date - Day-of-month (1-based).
2885
+ */
2587
2886
  createDate(year, month, date) {
2588
- // Check for invalid month and date (except upper bound on date which we have to check after
2589
- // creating the Date).
2590
2887
  if (month < 0 || month > 11) {
2591
2888
  throw Error(`Invalid month index "${month}". Month index has to be between 0 and 11.`);
2592
2889
  }
@@ -2598,32 +2895,41 @@ class DateFnsAdapter extends DateAdapter {
2598
2895
  const result = new Date();
2599
2896
  result.setFullYear(year, month, date);
2600
2897
  result.setHours(0, 0, 0, 0);
2601
- const result2 = new TZDate(result, "Europe/Rome");
2602
- // Check that the date wasn't above the upper bound for the month, causing the month to overflow
2898
+ const result2 = new TZDate(result, 'Europe/Rome');
2603
2899
  if (result2.getMonth() !== month) {
2604
2900
  throw Error(`Invalid date "${date}" for month with index "${month}".`);
2605
2901
  }
2606
2902
  return result2;
2607
2903
  }
2904
+ /**
2905
+ * Returns today's date in the local timezone.
2906
+ */
2608
2907
  today() {
2609
2908
  return new Date();
2610
2909
  }
2910
+ /**
2911
+ * Parses a value into a `Date`.
2912
+ * - Strings are first attempted as ISO 8601, then matched against each format in `parseFormat`.
2913
+ * - Numbers are treated as Unix timestamps (milliseconds).
2914
+ * - Existing `Date` instances are cloned.
2915
+ * @param value - The value to parse.
2916
+ * @param parseFormat - A format string or an array of format strings (date-fns tokens).
2917
+ * @returns A valid `Date` in `Europe/Rome`, an invalid sentinel, or `null` for unrecognised input.
2918
+ */
2611
2919
  parse(value, parseFormat) {
2612
2920
  if (typeof value === 'string' && value.length > 0) {
2613
- let iso8601Date = parseISO(value);
2921
+ const iso8601Date = parseISO(value);
2614
2922
  if (this.isValid(iso8601Date)) {
2615
- iso8601Date = new TZDate(iso8601Date, "Europe/Rome");
2616
- return iso8601Date;
2923
+ return new TZDate(iso8601Date, 'Europe/Rome');
2617
2924
  }
2618
2925
  const formats = Array.isArray(parseFormat) ? parseFormat : [parseFormat];
2619
- if (!parseFormat.length) {
2926
+ if (!formats.length) {
2620
2927
  throw Error('Formats array must not be empty.');
2621
2928
  }
2622
2929
  for (const currentFormat of formats) {
2623
- let fromFormat = parse(value, currentFormat, new Date(), { locale: this.locale });
2930
+ const fromFormat = parse(value, currentFormat, new Date(), { locale: this.locale });
2624
2931
  if (this.isValid(fromFormat)) {
2625
- fromFormat = new TZDate(fromFormat, "Europe/Rome");
2626
- return fromFormat;
2932
+ return new TZDate(fromFormat, 'Europe/Rome');
2627
2933
  }
2628
2934
  }
2629
2935
  return this.invalid();
@@ -2634,30 +2940,56 @@ class DateFnsAdapter extends DateAdapter {
2634
2940
  else if (value instanceof Date) {
2635
2941
  return this.clone(value);
2636
2942
  }
2637
- return undefined;
2943
+ return null;
2638
2944
  }
2945
+ /**
2946
+ * Formats a `Date` using the given date-fns display format string.
2947
+ * Throws an `Error` when `date` is not valid.
2948
+ * @param date - The date to format.
2949
+ * @param displayFormat - A date-fns format string (e.g. `'P'`, `'LLL uuuu'`).
2950
+ */
2639
2951
  format(date, displayFormat) {
2640
2952
  if (!this.isValid(date)) {
2641
2953
  throw Error('DateFnsAdapter: Cannot format invalid date.');
2642
2954
  }
2643
2955
  return format(date, displayFormat, { locale: this.locale });
2644
2956
  }
2957
+ /**
2958
+ * Adds the given number of whole years to a date.
2959
+ * @param date - The base date.
2960
+ * @param years - Number of years to add (can be negative).
2961
+ */
2645
2962
  addCalendarYears(date, years) {
2646
2963
  return addYears(date, years);
2647
2964
  }
2965
+ /**
2966
+ * Adds the given number of whole months to a date.
2967
+ * @param date - The base date.
2968
+ * @param months - Number of months to add (can be negative).
2969
+ */
2648
2970
  addCalendarMonths(date, months) {
2649
2971
  return addMonths(date, months);
2650
2972
  }
2973
+ /**
2974
+ * Adds the given number of whole days to a date.
2975
+ * @param date - The base date.
2976
+ * @param days - Number of days to add (can be negative).
2977
+ */
2651
2978
  addCalendarDays(date, days) {
2652
2979
  return addDays(date, days);
2653
2980
  }
2981
+ /**
2982
+ * Serialises a date to an ISO 8601 date string (`yyyy-MM-dd`).
2983
+ * @param date - The date to serialise.
2984
+ */
2654
2985
  toIso8601(date) {
2655
2986
  return formatISO(date, { representation: 'date' });
2656
2987
  }
2657
2988
  /**
2658
- * Returns the given value if given a valid Date or undefined. Deserializes valid ISO 8601 strings
2659
- * (https://www.ietf.org/rfc/rfc3339.txt) into valid Dates and empty string into undefined. Returns an
2660
- * invalid date for all other values.
2989
+ * Returns the given value when it is a valid `Date`, or `null` for an empty string.
2990
+ * Deserialises valid ISO 8601 strings into `Date` instances.
2991
+ * Delegates all other values to the base-class implementation.
2992
+ * @param value - The raw value to deserialise.
2661
2993
  */
2662
2994
  deserialize(value) {
2663
2995
  if (typeof value === 'string') {
@@ -2671,12 +3003,23 @@ class DateFnsAdapter extends DateAdapter {
2671
3003
  }
2672
3004
  return super.deserialize(value);
2673
3005
  }
3006
+ /**
3007
+ * Returns `true` when `obj` is an instance of `Date`.
3008
+ * @param obj - The object to test.
3009
+ */
2674
3010
  isDateInstance(obj) {
2675
3011
  return isDate(obj);
2676
3012
  }
3013
+ /**
3014
+ * Returns `true` when `date` represents a valid point in time.
3015
+ * @param date - The date to validate.
3016
+ */
2677
3017
  isValid(date) {
2678
3018
  return isValid(date);
2679
3019
  }
3020
+ /**
3021
+ * Returns a sentinel `Date` that represents an invalid date (`new Date(NaN)`).
3022
+ */
2680
3023
  invalid() {
2681
3024
  return new Date(NaN);
2682
3025
  }
@@ -2712,21 +3055,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
2712
3055
  }]
2713
3056
  }] });
2714
3057
 
3058
+ /**
3059
+ * Directive that removes focus from the host element after it is clicked,
3060
+ * preventing the browser from keeping a visible focus ring post-interaction.
3061
+ * Apply `removeFocus` to any focusable element (e.g. a button).
3062
+ */
2715
3063
  class RemoveFocusDirective {
2716
3064
  constructor() {
2717
3065
  this.elementRef = inject(ElementRef);
2718
3066
  }
2719
- onClick(_event) {
2720
- // Controllo null safety
2721
- if (this.elementRef?.nativeElement && typeof this.elementRef.nativeElement.blur === 'function') {
2722
- // Piccolo delay per permettere al click di completarsi
2723
- setTimeout(() => {
2724
- this.elementRef.nativeElement.blur();
2725
- }, 0);
3067
+ /**
3068
+ * Handles click events on the host element and blurs it on the next event-loop tick
3069
+ * so that the click action completes before focus is removed.
3070
+ */
3071
+ onClick() {
3072
+ const el = this.elementRef.nativeElement;
3073
+ if (el && typeof el.blur === 'function') {
3074
+ setTimeout(() => el.blur(), 0);
2726
3075
  }
2727
3076
  }
2728
3077
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RemoveFocusDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
2729
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RemoveFocusDirective, isStandalone: true, selector: "[removeFocus]", host: { listeners: { "click": "onClick($event)" } }, ngImport: i0 }); }
3078
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RemoveFocusDirective, isStandalone: true, selector: "[removeFocus]", host: { listeners: { "click": "onClick()" } }, ngImport: i0 }); }
2730
3079
  }
2731
3080
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RemoveFocusDirective, decorators: [{
2732
3081
  type: Directive,
@@ -2736,13 +3085,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
2736
3085
  }]
2737
3086
  }], propDecorators: { onClick: [{
2738
3087
  type: HostListener,
2739
- args: ['click', ['$event']]
3088
+ args: ['click']
2740
3089
  }] } });
2741
3090
 
3091
+ /**
3092
+ * Pipe that converts a Markdown string to sanitized HTML using `SystemUtils.markdownToHtml`.
3093
+ *
3094
+ * Usage: `{{ text | formatMarkdown }}`
3095
+ */
2742
3096
  class FormatMarkdownPipe {
2743
3097
  constructor() {
2744
3098
  this.sanitizer = inject(DomSanitizer);
2745
3099
  }
3100
+ /**
3101
+ * Transforms a Markdown string into sanitized HTML.
3102
+ * @param value - The Markdown input to convert. Treated as an empty string when `undefined`.
3103
+ * @returns A `SafeHtml` value that can be rendered with `[innerHTML]`.
3104
+ */
2746
3105
  transform(value) {
2747
3106
  return this.sanitizer.bypassSecurityTrustHtml(SystemUtils.markdownToHtml(value ?? ''));
2748
3107
  }