@ardium-pl/devkit 8.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +25 -0
  2. package/fesm2022/ardium-pl-devkit.mjs +3874 -0
  3. package/fesm2022/ardium-pl-devkit.mjs.map +1 -0
  4. package/index.d.ts +5 -0
  5. package/lib/click-outside/click-outside.directive.d.ts +14 -0
  6. package/lib/click-outside/click-outside.module.d.ts +8 -0
  7. package/lib/click-outside/index.d.ts +2 -0
  8. package/lib/coercion/array.d.ts +21 -0
  9. package/lib/coercion/boolean.d.ts +6 -0
  10. package/lib/coercion/date.d.ts +5 -0
  11. package/lib/coercion/index.d.ts +4 -0
  12. package/lib/coercion/number.d.ts +20 -0
  13. package/lib/dom-boxes/dom-boxes.d.ts +7 -0
  14. package/lib/dom-boxes/index.d.ts +1 -0
  15. package/lib/escape-html/escape-html.d.ts +1 -0
  16. package/lib/escape-html/escape-html.module.d.ts +8 -0
  17. package/lib/escape-html/escape-html.pipe.d.ts +7 -0
  18. package/lib/escape-html/index.d.ts +3 -0
  19. package/lib/file-pipes/file-pipes.defaults.d.ts +8 -0
  20. package/lib/file-pipes/file-pipes.module.d.ts +10 -0
  21. package/lib/file-pipes/fileext.pipe.d.ts +39 -0
  22. package/lib/file-pipes/filename.pipe.d.ts +38 -0
  23. package/lib/file-pipes/filesize.pipe.d.ts +50 -0
  24. package/lib/file-pipes/index.d.ts +5 -0
  25. package/lib/file-system/file-system.service.d.ts +69 -0
  26. package/lib/file-system/file-system.types.d.ts +186 -0
  27. package/lib/file-system/index.d.ts +2 -0
  28. package/lib/find-functions/find-suggestion.d.ts +3 -0
  29. package/lib/find-functions/index.d.ts +1 -0
  30. package/lib/forms/index.d.ts +1 -0
  31. package/lib/forms/track-form-control/index.d.ts +1 -0
  32. package/lib/forms/track-form-control/track-form-control.d.ts +83 -0
  33. package/lib/hold/hold.defaults.d.ts +9 -0
  34. package/lib/hold/hold.directive.d.ts +24 -0
  35. package/lib/hold/hold.module.d.ts +8 -0
  36. package/lib/hold/index.d.ts +3 -0
  37. package/lib/http-service/_types.d.ts +20 -0
  38. package/lib/http-service/_utils.d.ts +1 -0
  39. package/lib/http-service/http.service.d.ts +2574 -0
  40. package/lib/http-service/index.d.ts +1 -0
  41. package/lib/infinite-scroll/index.d.ts +3 -0
  42. package/lib/infinite-scroll/infinite-scroll.defaults.d.ts +8 -0
  43. package/lib/infinite-scroll/infinite-scroll.directive.d.ts +24 -0
  44. package/lib/infinite-scroll/infinite-scroll.module.d.ts +8 -0
  45. package/lib/keyboard/index.d.ts +2 -0
  46. package/lib/keyboard/keyboard.service.d.ts +60 -0
  47. package/lib/keyboard/keyboard.types.d.ts +13 -0
  48. package/lib/keyboard/types-internal.d.ts +20 -0
  49. package/lib/keyboard/utils.d.ts +3 -0
  50. package/lib/relative-pos/index.d.ts +1 -0
  51. package/lib/relative-pos/relative-pos.d.ts +24 -0
  52. package/lib/signals/array/array-signal.d.ts +123 -0
  53. package/lib/signals/array/index.d.ts +1 -0
  54. package/lib/signals/counter/counter-signal.d.ts +67 -0
  55. package/lib/signals/counter/index.d.ts +1 -0
  56. package/lib/signals/debounced/debounced-signal.d.ts +15 -0
  57. package/lib/signals/debounced/index.d.ts +1 -0
  58. package/lib/signals/map/index.d.ts +1 -0
  59. package/lib/signals/map/map-signal.d.ts +97 -0
  60. package/lib/signals/persistent/index.d.ts +1 -0
  61. package/lib/signals/persistent/persistent-signal.d.ts +106 -0
  62. package/lib/signals/query-param/index.d.ts +1 -0
  63. package/lib/signals/query-param/query-param-signal.d.ts +35 -0
  64. package/lib/signals/queue/index.d.ts +1 -0
  65. package/lib/signals/queue/queue-signal.d.ts +77 -0
  66. package/lib/signals/set/index.d.ts +1 -0
  67. package/lib/signals/set/set-signal.d.ts +76 -0
  68. package/lib/signals/stack/index.d.ts +1 -0
  69. package/lib/signals/stack/stack-signal.d.ts +77 -0
  70. package/lib/signals/throttled/index.d.ts +1 -0
  71. package/lib/signals/throttled/throttled-signal.d.ts +15 -0
  72. package/lib/signals/tuple/index.d.ts +1 -0
  73. package/lib/signals/tuple/tuple-signal.d.ts +60 -0
  74. package/lib/validators/index.d.ts +1 -0
  75. package/lib/validators/validators.d.ts +612 -0
  76. package/lib/viewport-observer/index.d.ts +3 -0
  77. package/lib/viewport-observer/utils.d.ts +15 -0
  78. package/lib/viewport-observer/viewport-observer-ref.d.ts +47 -0
  79. package/lib/viewport-observer/viewport-observer.defaults.d.ts +7 -0
  80. package/lib/viewport-observer/viewport-observer.service.d.ts +23 -0
  81. package/package.json +30 -0
  82. package/public-api.d.ts +26 -0
@@ -0,0 +1,3874 @@
1
+ import * as i0 from '@angular/core';
2
+ import { Injectable, inject, signal, computed, ElementRef, InjectionToken, output, Directive, HostListener, NgModule, Pipe, input, effect, model, untracked, Injector, runInInjectionContext, DestroyRef } from '@angular/core';
3
+ import { isArray, isNumber, isNull, isAnyString, isFunction, isDefined } from 'simple-bool';
4
+ import { HttpParams, HttpClient } from '@angular/common/http';
5
+ import { Subject, map, filter, merge, throttleTime, debounceTime, BehaviorSubject, isObservable } from 'rxjs';
6
+ import * as i1 from '@angular/platform-browser';
7
+ import { CommonModule } from '@angular/common';
8
+ import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
9
+ import { Router, NavigationEnd } from '@angular/router';
10
+ import { isFormControl, isFormArray, isFormGroup, isFormRecord, NgControl, ValueChangeEvent, TouchedChangeEvent, PristineChangeEvent, StatusChangeEvent, Validators } from '@angular/forms';
11
+ import { diceCoefficient } from 'dice-coefficient';
12
+
13
+ /**
14
+ * Enumeration of file system methods.
15
+ */
16
+ const FileSystemMethod = {
17
+ /**
18
+ * Prefer using the File System Access API, or if not supported, use a cross-browser compatible method.
19
+ *
20
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API File System Access API}
21
+ */
22
+ PreferFileSystem: 'preferFileSystem',
23
+ /**
24
+ * Always use a cross-browser compatible method.
25
+ */
26
+ CrossBrowser: 'crossBrowser',
27
+ };
28
+ /**
29
+ * Enumeration of file system start directories.
30
+ */
31
+ const FileSystemStartDirectory = {
32
+ /**
33
+ * Always start in the Desktop directory.
34
+ */
35
+ Desktop: 'desktop',
36
+ /**
37
+ * Always start in the Documents directory.
38
+ */
39
+ Documents: 'documents',
40
+ /**
41
+ * Always start in the Downloads directory.
42
+ */
43
+ Downloads: 'downloads',
44
+ /**
45
+ * Always start in the Music directory.
46
+ */
47
+ Music: 'music',
48
+ /**
49
+ * Always start in the Pictures directory.
50
+ */
51
+ Pictures: 'pictures',
52
+ /**
53
+ * Always start in the Videos directory.
54
+ */
55
+ Videos: 'videos',
56
+ };
57
+
58
+ const DEFAULT_SAVE_OPTIONS = {
59
+ fileName: 'download',
60
+ method: FileSystemMethod.PreferFileSystem,
61
+ };
62
+ const DEFAULT_REQUEST_OPTIONS = {
63
+ method: FileSystemMethod.PreferFileSystem,
64
+ multiple: false,
65
+ };
66
+ /**
67
+ * Service for handling file system operations, including saving, uploading, and reading files.
68
+ *
69
+ * This service leverages the File System Access API when available and falls back to traditional
70
+ * methods when necessary. It provides methods to check for API support, save files, request file uploads,
71
+ * and read file contents.
72
+ *
73
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API File System Access API}
74
+ */
75
+ class FileSystemService {
76
+ /**
77
+ * Checks if the File System Access API is supported for the specified method.
78
+ *
79
+ * @param method - The file system method to check ('showSaveFilePicker' or 'showOpenFilePicker').
80
+ * @returns True if the specified method is supported, otherwise false.
81
+ *
82
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API File System Access API}
83
+ */
84
+ isFileSystemAPISupported(method) {
85
+ try {
86
+ // isn't in an iframe && browser version is high enough
87
+ return window.self === window.top && method in window;
88
+ }
89
+ catch (err) {
90
+ return false;
91
+ }
92
+ }
93
+ //! saving files
94
+ /**
95
+ * Saves data as a file using the File System Access API or a fallback method.
96
+ *
97
+ * @param data - The data to be saved, either as a string or a Blob.
98
+ * @param options - Options for saving the file, such as file name and save method.
99
+ * @returns A promise that resolves to true if the file was saved successfully, otherwise false.
100
+ *
101
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API File System Access API}
102
+ */
103
+ async saveAs(data, options = {}) {
104
+ options = {
105
+ ...DEFAULT_SAVE_OPTIONS,
106
+ ...options,
107
+ };
108
+ // coerce string to blob if needed
109
+ if (typeof data == 'string') {
110
+ data = new Blob([data], {
111
+ type: 'text/plain',
112
+ });
113
+ }
114
+ // use the File System Access API if supported & preferred
115
+ if (options.method == FileSystemMethod.PreferFileSystem &&
116
+ this.isFileSystemAPISupported('showSaveFilePicker')) {
117
+ if (!options.types)
118
+ options.types = [];
119
+ // use the File System Access API
120
+ try {
121
+ const handle = await window.showSaveFilePicker({
122
+ id: options.directoryId,
123
+ startIn: options.startDirectory,
124
+ suggestedName: options.fileName,
125
+ types: options.types,
126
+ });
127
+ const writable = await handle.createWritable();
128
+ await writable.write(data);
129
+ await writable.close();
130
+ return true;
131
+ }
132
+ catch (err) {
133
+ // fail silently if the user has simply canceled the dialog.
134
+ const error = err;
135
+ if (error.name !== 'AbortError') {
136
+ console.error(error.name, error.message);
137
+ }
138
+ return false;
139
+ }
140
+ }
141
+ // fallback if the File System Access API is not supported
142
+ // or the user doesn't want to use it
143
+ const blobURL = URL.createObjectURL(data);
144
+ const a = document.createElement('a');
145
+ a.href = blobURL;
146
+ a.download = options.fileName;
147
+ a.style.display = 'none';
148
+ document.body.appendChild(a);
149
+ a.click();
150
+ //remove the element from the DOM
151
+ return await new Promise((resolve) => setTimeout(() => {
152
+ URL.revokeObjectURL(blobURL);
153
+ document.body.removeChild(a);
154
+ resolve(true);
155
+ }, 1000));
156
+ }
157
+ //! opening files
158
+ /**
159
+ * Requests a file upload using the File System Access API or a fallback method.
160
+ *
161
+ * @param options - Options for requesting the file upload, such as accepted file types and method.
162
+ * @returns A promise that resolves to the selected file or files, or null if the operation was canceled.
163
+ *
164
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API File System Access API}
165
+ */
166
+ async requestFileUpload(options = {}) {
167
+ options = {
168
+ ...DEFAULT_REQUEST_OPTIONS,
169
+ ...options,
170
+ };
171
+ // use the File System Access API if supported & preferred
172
+ if (options.method == 'preferFileSystem' &&
173
+ this.isFileSystemAPISupported('showOpenFilePicker')) {
174
+ try {
175
+ // coerce the options.accept into a valid options.types object
176
+ if (options.accept) {
177
+ if (typeof options.accept == 'string') {
178
+ options.accept = options.accept.split(',');
179
+ }
180
+ for (const accept of options.accept) {
181
+ if (!accept.match(/^\.[a-z0-9]+$/)) {
182
+ throw new Error(`DKT-FT0010: "accept" property in FileSystemService must be a list of valid file extensions. Value "${accept}" is not a valid extension.`);
183
+ }
184
+ }
185
+ if (!options.types)
186
+ options.types = [];
187
+ const accept = {
188
+ 'application/octet-stream': options.accept,
189
+ };
190
+ options.types.push({ accept: accept });
191
+ }
192
+ // open the dialog box
193
+ const handles = await window.showOpenFilePicker({
194
+ id: options.directoryId,
195
+ startIn: options.startDirectory,
196
+ types: options.types,
197
+ multiple: options.multiple,
198
+ });
199
+ const fileArray = (await Promise.all(handles.map(async (handle) => {
200
+ const file = await handle.getFile();
201
+ file.handle = handle;
202
+ return file;
203
+ })));
204
+ if (!options.multiple) {
205
+ return fileArray[0];
206
+ }
207
+ return fileArray;
208
+ }
209
+ catch (err) {
210
+ // fail silently if the user has simply canceled the dialog.
211
+ const error = err;
212
+ if (error.name !== 'AbortError') {
213
+ console.error(error.name, error.message);
214
+ }
215
+ return null;
216
+ }
217
+ }
218
+ // fallback if the File System Access API is not supported
219
+ // or the user doesn't want to use it
220
+ // coerce options.accept into a string
221
+ if (isArray(options.accept)) {
222
+ options.accept = options.accept.join(',');
223
+ }
224
+ else {
225
+ options.accept = options.accept ?? '*';
226
+ }
227
+ const input = document.createElement('input');
228
+ input.type = 'file';
229
+ input.accept = options.accept;
230
+ input.multiple = options.multiple ?? false;
231
+ input.style.display = 'none';
232
+ document.body.appendChild(input);
233
+ input.click();
234
+ const fileArray = await new Promise((resolve) => {
235
+ input.onchange = () => {
236
+ resolve(input.files && input.files.length > 0
237
+ ? Array.from(input.files)
238
+ : null);
239
+ document.body.removeChild(input);
240
+ };
241
+ });
242
+ if (options.multiple || !fileArray) {
243
+ return fileArray;
244
+ }
245
+ return fileArray[0];
246
+ }
247
+ async readFile(file, readAs = 'text', encoding = 'UTF-8') {
248
+ if (readAs == 'text') {
249
+ return new Promise((resolve) => {
250
+ const reader = new FileReader();
251
+ reader.readAsText(file, encoding);
252
+ reader.onload = function (e) {
253
+ resolve(e.target.result);
254
+ };
255
+ });
256
+ }
257
+ return new Promise((resolve) => {
258
+ const reader = new FileReader();
259
+ reader.readAsArrayBuffer(file);
260
+ reader.onload = function (e) {
261
+ resolve(e.target.result);
262
+ };
263
+ });
264
+ }
265
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: FileSystemService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
266
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: FileSystemService, providedIn: 'root' }); }
267
+ }
268
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: FileSystemService, decorators: [{
269
+ type: Injectable,
270
+ args: [{
271
+ providedIn: 'root',
272
+ }]
273
+ }] });
274
+
275
+ function normalizePath(input) {
276
+ if (!Array.isArray(input)) {
277
+ input = input.trim();
278
+ return input.replace(/\/\/+$/g, "/");
279
+ }
280
+ return input
281
+ .filter(Boolean)
282
+ .map((part, index) => {
283
+ if (index === 0) {
284
+ return part.replace(/\/+$/g, "");
285
+ }
286
+ return part.replace(/^\/+|\/+$/g, "");
287
+ })
288
+ .join("/")
289
+ .replace(/\/\/+/g, "/");
290
+ }
291
+
292
+ const FAULTY_URL_REGEX = /https?:\/\//;
293
+ function getUrl(apiUrl, url) {
294
+ url = normalizePath(url);
295
+ const finalUrl = apiUrl + url.replace(/^\//, '');
296
+ if (FAULTY_URL_REGEX.test(url)) {
297
+ console.warn(`DKT-WA0020: The url "${url}" passed into custom HTTP Service seems to be faulty, as it would produce a request to "${finalUrl}". Custom HTTP Service instances prepend an API url, and thus only accept relative url paths.`);
298
+ }
299
+ return finalUrl;
300
+ }
301
+ function getOpts(defaultOptions, options) {
302
+ return { ...defaultOptions, ...(options ?? {}) };
303
+ }
304
+ function convertToHttpParams(params) {
305
+ if (params instanceof HttpParams || !params) {
306
+ return params;
307
+ }
308
+ let httpParams = new HttpParams();
309
+ for (const key of Object.keys(params)) {
310
+ const value = params[key];
311
+ if (value == null) {
312
+ continue;
313
+ }
314
+ if (Array.isArray(value)) {
315
+ value.forEach((item) => {
316
+ httpParams = httpParams.append(key, item.toString());
317
+ });
318
+ }
319
+ else {
320
+ httpParams = httpParams.append(key, value.toString());
321
+ }
322
+ }
323
+ return httpParams;
324
+ }
325
+ function createHttpService(apiUrl, defaultOptions = {}) {
326
+ let http;
327
+ return class {
328
+ constructor() {
329
+ this.apiUrl = apiUrl;
330
+ http = inject(HttpClient);
331
+ }
332
+ request(methodOrReq, url, options) {
333
+ if (typeof methodOrReq === 'string') {
334
+ const finalUrl = getUrl(apiUrl, url);
335
+ const finalOptions = getOpts(defaultOptions, options);
336
+ if (finalOptions.params) {
337
+ finalOptions.params = convertToHttpParams(finalOptions.params);
338
+ }
339
+ return http.request(methodOrReq, finalUrl, finalOptions);
340
+ }
341
+ return http.request(methodOrReq);
342
+ }
343
+ delete(url, options) {
344
+ const finalUrl = getUrl(apiUrl, url);
345
+ const finalOptions = getOpts(defaultOptions, options);
346
+ if (finalOptions.params) {
347
+ finalOptions.params = convertToHttpParams(finalOptions.params);
348
+ }
349
+ return http.delete(finalUrl, finalOptions);
350
+ }
351
+ get(url, options) {
352
+ const finalUrl = getUrl(apiUrl, url);
353
+ const finalOptions = getOpts(defaultOptions, options);
354
+ if (finalOptions.params) {
355
+ finalOptions.params = convertToHttpParams(finalOptions.params);
356
+ }
357
+ return http.get(finalUrl, finalOptions);
358
+ }
359
+ head(url, options) {
360
+ const finalUrl = getUrl(apiUrl, url);
361
+ const finalOptions = getOpts(defaultOptions, options);
362
+ if (finalOptions.params) {
363
+ finalOptions.params = convertToHttpParams(finalOptions.params);
364
+ }
365
+ return http.head(finalUrl, finalOptions);
366
+ }
367
+ jsonp(url, callbackParam) {
368
+ url = getUrl(apiUrl, url);
369
+ return http.jsonp(url, callbackParam);
370
+ }
371
+ options(url, options) {
372
+ const finalUrl = getUrl(apiUrl, url);
373
+ const finalOptions = getOpts(defaultOptions, options);
374
+ if (finalOptions.params) {
375
+ finalOptions.params = convertToHttpParams(finalOptions.params);
376
+ }
377
+ return http.options(finalUrl, finalOptions);
378
+ }
379
+ patch(url, body, options) {
380
+ const finalUrl = getUrl(apiUrl, url);
381
+ const finalOptions = getOpts(defaultOptions, options);
382
+ if (finalOptions.params) {
383
+ finalOptions.params = convertToHttpParams(finalOptions.params);
384
+ }
385
+ return http.patch(finalUrl, body, finalOptions);
386
+ }
387
+ post(url, body, options) {
388
+ const finalUrl = getUrl(apiUrl, url);
389
+ const finalOptions = getOpts(defaultOptions, options);
390
+ if (finalOptions.params) {
391
+ finalOptions.params = convertToHttpParams(finalOptions.params);
392
+ }
393
+ return http.post(finalUrl, body, finalOptions);
394
+ }
395
+ put(url, body, options) {
396
+ const finalUrl = getUrl(apiUrl, url);
397
+ const finalOptions = getOpts(defaultOptions, options);
398
+ if (finalOptions.params) {
399
+ finalOptions.params = convertToHttpParams(finalOptions.params);
400
+ }
401
+ return http.put(finalUrl, body, finalOptions);
402
+ }
403
+ };
404
+ }
405
+
406
+ const _ExpectedModifierKeySide = {
407
+ None: 'none',
408
+ Left: 'left',
409
+ Right: 'right',
410
+ Any: 'any',
411
+ };
412
+
413
+ function trySettingModifierKey(modifierKeyName, keyCode, rawKeyCode, previouslyFoundKeyCode, currentKeyState) {
414
+ if (keyCode === modifierKeyName) {
415
+ if (previouslyFoundKeyCode) {
416
+ console.error(new Error(`DKT-NF0030: A keyboard shortcut definition cannot contain two modifier keys of the same type. Got "${previouslyFoundKeyCode}" and "${rawKeyCode}"; only the first one will be included.`));
417
+ return [currentKeyState, previouslyFoundKeyCode, true];
418
+ }
419
+ return [_ExpectedModifierKeySide.Any, keyCode, true];
420
+ }
421
+ if (keyCode === modifierKeyName + 'left') {
422
+ if (previouslyFoundKeyCode) {
423
+ console.error(new Error(`DKT-NF0030: A keyboard shortcut definition cannot contain two modifier keys of the same type. Got "${previouslyFoundKeyCode}" and "${rawKeyCode}"; only the first one will be included.`));
424
+ return [currentKeyState, previouslyFoundKeyCode, true];
425
+ }
426
+ return [_ExpectedModifierKeySide.Left, keyCode, true];
427
+ }
428
+ if (keyCode === modifierKeyName + 'right') {
429
+ if (previouslyFoundKeyCode) {
430
+ console.error(new Error(`DKT-NF0030: A keyboard shortcut definition cannot contain two modifier keys of the same type. Got "${previouslyFoundKeyCode}" and "${rawKeyCode}"; only the first one will be included.`));
431
+ return [currentKeyState, previouslyFoundKeyCode, true];
432
+ }
433
+ return [_ExpectedModifierKeySide.Right, keyCode, true];
434
+ }
435
+ return [currentKeyState, previouslyFoundKeyCode, false];
436
+ }
437
+ function isModifierKeyInWrongState(state, expectedState) {
438
+ if (expectedState === _ExpectedModifierKeySide.Any)
439
+ return !state.left && !state.right;
440
+ let expectedLeft = false, expectedRight = false;
441
+ switch (expectedState) {
442
+ case _ExpectedModifierKeySide.None:
443
+ break;
444
+ case _ExpectedModifierKeySide.Left:
445
+ expectedLeft = true;
446
+ break;
447
+ case _ExpectedModifierKeySide.Right:
448
+ expectedRight = true;
449
+ break;
450
+ }
451
+ return expectedLeft !== state.left || expectedRight !== state.right;
452
+ }
453
+
454
+ class KeyboardService {
455
+ constructor(rendererFactory) {
456
+ this._isLeftCtrlHeld = signal(false);
457
+ this._isRightCtrlHeld = signal(false);
458
+ this.isLeftCtrlHeld = this._isLeftCtrlHeld.asReadonly();
459
+ this.isRightCtrlHeld = this._isRightCtrlHeld.asReadonly();
460
+ this.isCtrlHeld = computed(() => this._isLeftCtrlHeld() || this._isRightCtrlHeld());
461
+ this._isLeftAltHeld = signal(false);
462
+ this._isRightAltHeld = signal(false);
463
+ this.isAltHeld = this._isLeftAltHeld.asReadonly();
464
+ this.isAltGraphHeld = this._isRightAltHeld.asReadonly();
465
+ this._isLeftShiftHeld = signal(false);
466
+ this._isRightShiftHeld = signal(false);
467
+ this.isLeftShiftHeld = this._isLeftShiftHeld.asReadonly();
468
+ this.isRightShiftHeld = this._isRightShiftHeld.asReadonly();
469
+ this.isShiftHeld = computed(() => this._isLeftShiftHeld() || this._isRightShiftHeld());
470
+ // "⊞ Win" on Windows, "⌘ Command" on Mac
471
+ this._isLeftMetaHeld = signal(false);
472
+ this._isRightMetaHeld = signal(false);
473
+ this.isLeftMetaHeld = this._isLeftMetaHeld.asReadonly();
474
+ this.isRightMetaHeld = this._isRightMetaHeld.asReadonly();
475
+ this.isMetaHeld = computed(() => this._isLeftMetaHeld() || this._isRightMetaHeld());
476
+ this.isAnyModifierKeyHeld = computed(() => this.isCtrlHeld() ||
477
+ this.isAltHeld() ||
478
+ this.isAltGraphHeld() ||
479
+ this.isShiftHeld() ||
480
+ this.isMetaHeld());
481
+ this._capsLockState = signal(undefined);
482
+ this.capsLockState = this._capsLockState.asReadonly();
483
+ this._numLockState = signal(undefined);
484
+ this.numLockState = this._numLockState.asReadonly();
485
+ this._scrollLockState = signal(undefined);
486
+ this.scrollLockState = this._scrollLockState.asReadonly();
487
+ //! key subjects
488
+ this._shortcutSubject$ = new Subject();
489
+ this.anyShortcut$ = this._shortcutSubject$.pipe(map((v) => v.publicData));
490
+ this._keySubject$ = new Subject();
491
+ this.anyKeyPress$ = this._keySubject$.asObservable();
492
+ this._keyStateSubject$ = new Subject();
493
+ this.anyKeyState$ = this._keyStateSubject$.asObservable();
494
+ this._KEY_CODE_SIGNALS_MAP = new Map([
495
+ ['ControlLeft', this._isLeftCtrlHeld],
496
+ ['ControlRight', this._isRightCtrlHeld],
497
+ ['AltLeft', this._isLeftAltHeld],
498
+ ['AltRight', this._isRightAltHeld],
499
+ ['ShiftLeft', this._isLeftShiftHeld],
500
+ ['ShiftRight', this._isRightShiftHeld],
501
+ ['MetaLeft', this._isLeftMetaHeld],
502
+ ['MetaRight', this._isRightMetaHeld],
503
+ ]);
504
+ this.renderer = rendererFactory.createRenderer(null, null);
505
+ //listen to events
506
+ this.removeListenFuncKeydown = this.renderer.listen('document', 'keydown', (e) => this._onKeydown(e));
507
+ this.removeListenFuncKeyup = this.renderer.listen('document', 'keyup', (e) => this._onKeyup(e));
508
+ this.removeListenFuncWindowblur = this.renderer.listen('window', 'blur', () => this._resetAllKeys());
509
+ }
510
+ ngOnDestroy() {
511
+ //destroy event listeners
512
+ this.removeListenFuncKeydown();
513
+ this.removeListenFuncKeyup();
514
+ this.removeListenFuncWindowblur();
515
+ }
516
+ _resetAllKeys() {
517
+ this._isLeftCtrlHeld.set(false);
518
+ this._isRightCtrlHeld.set(false);
519
+ this._isLeftAltHeld.set(false);
520
+ this._isRightAltHeld.set(false);
521
+ this._isLeftShiftHeld.set(false);
522
+ this._isRightShiftHeld.set(false);
523
+ this._isLeftMetaHeld.set(false);
524
+ this._isRightMetaHeld.set(false);
525
+ }
526
+ _demapKeyCode(code) {
527
+ code = code.replace(/[^a-z0-9]/gi, '').toLowerCase();
528
+ if (code.match(/^(left|right|up|down)$/))
529
+ return `Arrow${code.charAt(0).toUpperCase()}${code.substring(1)}`;
530
+ if (code.match(/^[a-z]$/))
531
+ return `Key${code.toUpperCase()}`;
532
+ if (code.match(/^\d$/))
533
+ return `Digit${code}`;
534
+ if (code === 'ctrl')
535
+ return 'control';
536
+ if (code === 'ctrlleft')
537
+ return 'controlleft';
538
+ if (code === 'alt')
539
+ return 'altleft';
540
+ if (code.match(/^altgr(aph)?$/))
541
+ return 'altright';
542
+ if (code.match(/^win(dows)?$/))
543
+ return 'meta';
544
+ if (code === 'cmd')
545
+ return 'meta';
546
+ return code;
547
+ }
548
+ _emitShortcut(event) {
549
+ const keys = [];
550
+ if (this._isLeftCtrlHeld())
551
+ keys.push('ControlLeft');
552
+ if (this._isRightCtrlHeld())
553
+ keys.push('ControlRight');
554
+ if (this._isLeftAltHeld())
555
+ keys.push('AltLeft');
556
+ if (this._isRightAltHeld())
557
+ keys.push('AltRight');
558
+ if (this._isLeftShiftHeld())
559
+ keys.push('ShiftLeft');
560
+ if (this._isRightShiftHeld())
561
+ keys.push('ShiftRight');
562
+ if (this._isLeftMetaHeld())
563
+ keys.push('MetaLeft');
564
+ if (this._isRightMetaHeld())
565
+ keys.push('MetaRight');
566
+ keys.push(event.code);
567
+ this._shortcutSubject$.next({
568
+ ctrl: {
569
+ left: this._isLeftCtrlHeld(),
570
+ right: this._isRightCtrlHeld(),
571
+ },
572
+ alt: {
573
+ left: this._isLeftAltHeld(),
574
+ right: this._isRightAltHeld(),
575
+ },
576
+ shift: {
577
+ left: this._isLeftShiftHeld(),
578
+ right: this._isRightShiftHeld(),
579
+ },
580
+ meta: {
581
+ left: this._isLeftMetaHeld(),
582
+ right: this._isRightMetaHeld(),
583
+ },
584
+ key: event.code.toLowerCase(),
585
+ publicData: {
586
+ keys,
587
+ event,
588
+ },
589
+ });
590
+ }
591
+ _emitKeydown(event) {
592
+ const key = event.code;
593
+ this._keySubject$.next({ key, event });
594
+ }
595
+ _emitKeyStateDown(event) {
596
+ const key = event.code;
597
+ this._keyStateSubject$.next({ key, event, isHeld: true });
598
+ }
599
+ _emitKeyStateUp(event) {
600
+ const key = event.code;
601
+ this._keyStateSubject$.next({ key, event, isHeld: false });
602
+ }
603
+ listenToShortcut(toMatch, treatAltGraphAsAltPlusCtrl = true) {
604
+ const expectedStates = {
605
+ ctrl: _ExpectedModifierKeySide.None,
606
+ alt: _ExpectedModifierKeySide.None,
607
+ shift: _ExpectedModifierKeySide.None,
608
+ meta: _ExpectedModifierKeySide.None,
609
+ };
610
+ let ctrlKey = null;
611
+ let altKey = null;
612
+ let isAltGraphExpected = false;
613
+ let shiftKey = null;
614
+ let metaKey = null;
615
+ let expectedKey = null;
616
+ let success = false;
617
+ for (const rawKey of toMatch) {
618
+ const key = this._demapKeyCode(rawKey).toLowerCase();
619
+ success = false;
620
+ [expectedStates.ctrl, ctrlKey, success] = trySettingModifierKey('control', key, rawKey, ctrlKey, expectedStates.ctrl);
621
+ if (success)
622
+ continue;
623
+ [expectedStates.alt, altKey, success] = trySettingModifierKey('alt', key, rawKey, altKey, expectedStates.alt);
624
+ if (key === 'altright') {
625
+ isAltGraphExpected = true;
626
+ }
627
+ if (success)
628
+ continue;
629
+ [expectedStates.shift, shiftKey, success] = trySettingModifierKey('shift', key, rawKey, shiftKey, expectedStates.shift);
630
+ if (success)
631
+ continue;
632
+ [expectedStates.meta, metaKey, success] = trySettingModifierKey('meta', key, rawKey, metaKey, expectedStates.meta);
633
+ if (success)
634
+ continue;
635
+ if (expectedKey) {
636
+ console.error(new Error(`DKT-NF0031: A keyboard shortcut definition cannot contain two final keys. Got "${expectedKey}" and "${rawKey}"; only the first one will be included.`));
637
+ break;
638
+ }
639
+ expectedKey = key;
640
+ }
641
+ if (treatAltGraphAsAltPlusCtrl && isAltGraphExpected) {
642
+ if (expectedStates.ctrl === _ExpectedModifierKeySide.Right) {
643
+ console.error(new Error(`DKT-NF0032: A keyboard shortcut definition that treats AltGraph as Alt+Ctrl cannot expect the user to press AltGraph and ControlRight keys (while not pressing ControlLeft), as pressing AltGraph automatically presses ControlLeft.`));
644
+ }
645
+ expectedStates.ctrl = _ExpectedModifierKeySide.Left;
646
+ }
647
+ return this._shortcutSubject$.pipe(filter((shortcut) => {
648
+ if (isModifierKeyInWrongState(shortcut.ctrl, expectedStates.ctrl))
649
+ return false;
650
+ if (isModifierKeyInWrongState(shortcut.alt, expectedStates.alt))
651
+ return false;
652
+ if (isModifierKeyInWrongState(shortcut.shift, expectedStates.shift))
653
+ return false;
654
+ if (isModifierKeyInWrongState(shortcut.meta, expectedStates.meta))
655
+ return false;
656
+ if (shortcut.key !== expectedKey)
657
+ return false;
658
+ return true;
659
+ }), map((v) => v.publicData));
660
+ }
661
+ listenToKey(code) {
662
+ const keyStr = this._demapKeyCode(code);
663
+ return this.anyKeyPress$.pipe(filter((key) => key.key == keyStr));
664
+ }
665
+ listenToKeyState(code) {
666
+ const keyStr = this._demapKeyCode(code);
667
+ return this.anyKeyState$.pipe(filter((key) => key.key == keyStr));
668
+ }
669
+ _onKeydown(event) {
670
+ let wasModifierKey = false;
671
+ const modifierSignal = this._KEY_CODE_SIGNALS_MAP.get(event.code);
672
+ if (modifierSignal) {
673
+ wasModifierKey = true;
674
+ modifierSignal.set(true);
675
+ }
676
+ this._updateLockKeyStates(event);
677
+ this._emitKeyStateDown(event);
678
+ if (!wasModifierKey && this.isAnyModifierKeyHeld()) {
679
+ this._emitShortcut(event);
680
+ }
681
+ else
682
+ this._emitKeydown(event);
683
+ }
684
+ _updateLockKeyStates(event) {
685
+ this._capsLockState.set(event.getModifierState?.('CapsLock'));
686
+ this._numLockState.set(event.getModifierState?.('NumLock'));
687
+ this._scrollLockState.set(event.getModifierState?.('ScrollLock'));
688
+ }
689
+ _onKeyup(event) {
690
+ const modifierSignal = this._KEY_CODE_SIGNALS_MAP.get(event.code);
691
+ modifierSignal?.set(false);
692
+ this._emitKeyStateUp(event);
693
+ }
694
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: KeyboardService, deps: [{ token: i1.ɵDomRendererFactory2 }], target: i0.ɵɵFactoryTarget.Injectable }); }
695
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: KeyboardService, providedIn: 'root' }); }
696
+ }
697
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: KeyboardService, decorators: [{
698
+ type: Injectable,
699
+ args: [{
700
+ providedIn: 'root',
701
+ }]
702
+ }], ctorParameters: () => [{ type: i1.ɵDomRendererFactory2 }] });
703
+
704
+ /**
705
+ * Custom RxJS operator that throttles incoming values but saves the last one when (emitted when the throttle duration ends). A combination of throttleTime and debounceTime.
706
+ * @param duration the throttle duration
707
+ * @returns an RxJS operator function
708
+ */
709
+ function throttleSaveLast(duration) {
710
+ return (source) => merge(source.pipe(throttleTime(duration)), source.pipe(debounceTime(duration))).pipe(throttleTime(0, undefined, { leading: true, trailing: false }));
711
+ }
712
+
713
+ var ViewportRelation;
714
+ (function (ViewportRelation) {
715
+ ViewportRelation["Above"] = "above";
716
+ ViewportRelation["PartiallyAbove"] = "partially-above";
717
+ ViewportRelation["Inside"] = "inside";
718
+ ViewportRelation["PartiallyBelow"] = "partially-below";
719
+ ViewportRelation["Below"] = "below";
720
+ ViewportRelation["InsideButTooLarge"] = "inside-too-large";
721
+ ViewportRelation["Undefined"] = "undefined";
722
+ })(ViewportRelation || (ViewportRelation = {}));
723
+ class ViewportMargins {
724
+ constructor(margin) {
725
+ this.top = signal(0);
726
+ this.bottom = signal(0);
727
+ if (isNumber(margin)) {
728
+ this.top.set(margin);
729
+ this.bottom.set(margin);
730
+ return;
731
+ }
732
+ this.top.set(margin?.top ?? 0);
733
+ this.bottom.set(margin?.bottom ?? 0);
734
+ }
735
+ }
736
+ class ArdViewportObserverRef {
737
+ constructor(element, update$, config) {
738
+ this.update$ = update$;
739
+ this._rawPosition = signal(undefined);
740
+ this.position = computed(() => {
741
+ const pos = this._rawPosition();
742
+ if (!pos)
743
+ return undefined;
744
+ return {
745
+ top: pos.top - this._margins.top(),
746
+ bottom: window.innerHeight - pos.bottom - this._margins.bottom(),
747
+ };
748
+ });
749
+ this.viewportRelation = computed(() => this._getNewRelation(this._rawPosition()));
750
+ this.isInViewport = computed(() => this.viewportRelation() === ViewportRelation.Undefined
751
+ ? undefined
752
+ : this.viewportRelation() === ViewportRelation.Inside ||
753
+ this.viewportRelation() === ViewportRelation.PartiallyAbove ||
754
+ this.viewportRelation() === ViewportRelation.PartiallyBelow);
755
+ this._isDestroyed = signal(false);
756
+ this.isDestroyed = this._isDestroyed.asReadonly();
757
+ this.element =
758
+ element instanceof ElementRef ? element.nativeElement : element;
759
+ setTimeout(() => {
760
+ this._updateViewportRelation();
761
+ }, 0);
762
+ this._throttleTime = config.throttleTime;
763
+ this._margins = new ViewportMargins(config.margin);
764
+ this.margins = {
765
+ top: this._margins.top.asReadonly(),
766
+ bottom: this._margins.bottom.asReadonly(),
767
+ };
768
+ this._scrollSubscription = this.update$
769
+ .pipe(throttleSaveLast(this._throttleTime))
770
+ .subscribe(() => this._updateViewportRelation());
771
+ }
772
+ _updateViewportRelation() {
773
+ const rect = this.element.getBoundingClientRect();
774
+ this._rawPosition.set({
775
+ top: rect.top,
776
+ bottom: rect.bottom,
777
+ });
778
+ }
779
+ _getNewRelation(rect) {
780
+ if (!rect)
781
+ return undefined;
782
+ const topThreshold = this._margins.top();
783
+ if (rect.bottom <= topThreshold) {
784
+ return ViewportRelation.Above;
785
+ }
786
+ if (rect.bottom > topThreshold && rect.top < topThreshold) {
787
+ return ViewportRelation.PartiallyAbove;
788
+ }
789
+ const bottomThreshold = window.innerHeight - this._margins.bottom();
790
+ if (rect.top > topThreshold && rect.bottom > bottomThreshold) {
791
+ return ViewportRelation.InsideButTooLarge;
792
+ }
793
+ if (rect.top < bottomThreshold && rect.bottom > bottomThreshold) {
794
+ return ViewportRelation.PartiallyBelow;
795
+ }
796
+ if (rect.top >= bottomThreshold) {
797
+ return ViewportRelation.Below;
798
+ }
799
+ return ViewportRelation.Inside;
800
+ }
801
+ recheck() {
802
+ this._updateViewportRelation();
803
+ }
804
+ destroy() {
805
+ if (this.isDestroyed())
806
+ return;
807
+ this._isDestroyed.set(true);
808
+ this._scrollSubscription.unsubscribe();
809
+ }
810
+ setMargin(top, bottom = top) {
811
+ this._margins.top.set(top);
812
+ this._margins.bottom.set(bottom);
813
+ this._updateViewportRelation();
814
+ return this;
815
+ }
816
+ }
817
+
818
+ const _viewportObserverDefaults = {
819
+ margin: 0,
820
+ throttleTime: 100,
821
+ };
822
+ const ARD_VIEWPORT_OBSERVER_DEFAULTS = new InjectionToken('ard-viewport-observer-defaults', {
823
+ factory: () => ({
824
+ ..._viewportObserverDefaults,
825
+ }),
826
+ });
827
+ function provideViewportObserverDefaults(config) {
828
+ return {
829
+ provide: ARD_VIEWPORT_OBSERVER_DEFAULTS,
830
+ useValue: { ..._viewportObserverDefaults, ...config },
831
+ };
832
+ }
833
+
834
+ class ArdiumViewportObserverService {
835
+ constructor(rendererFactory) {
836
+ this._lastWindowHeight = window.innerHeight;
837
+ this._DEFAULTS = inject(ARD_VIEWPORT_OBSERVER_DEFAULTS);
838
+ this._scrollOrResizeSubject = new Subject();
839
+ this.scrollOrResize$ = this._scrollOrResizeSubject.asObservable();
840
+ this._registeredObservers = [];
841
+ this.renderer = rendererFactory.createRenderer(null, null);
842
+ this.setScrollHost(document);
843
+ this._resizeCleanupFn = this.renderer.listen(window, 'resize', () => {
844
+ window.innerHeight !== this._lastWindowHeight &&
845
+ this._scrollOrResizeSubject.next();
846
+ });
847
+ }
848
+ setScrollHost(element) {
849
+ this._cleanupObservers();
850
+ this._scrollCleanupFn = this.renderer.listen(element, 'scroll', () => this._scrollOrResizeSubject.next());
851
+ }
852
+ observeElement(element, config) {
853
+ const finalConfig = { ...this._DEFAULTS, ...(config ?? {}) };
854
+ const vo = new ArdViewportObserverRef(element, this.scrollOrResize$, finalConfig);
855
+ this._registeredObservers.push(vo);
856
+ return vo;
857
+ }
858
+ observeById(id, config) {
859
+ const element = document.getElementById(id);
860
+ if (!element) {
861
+ throw new Error(`DKT-NF0001: Trying to observe an element by id, but the element with id "${id}" does not exist.`);
862
+ }
863
+ return this.observeElement(element, config);
864
+ }
865
+ observeByQuery(query, config) {
866
+ const element = document.querySelector(query);
867
+ if (!element) {
868
+ throw new Error(`DKT-NF0002: Trying to observe an element by query, but no element matching "${query}" exists.`);
869
+ }
870
+ return this.observeElement(element, config);
871
+ }
872
+ recheckAll() {
873
+ this._scrollOrResizeSubject.next();
874
+ }
875
+ _cleanupObservers() {
876
+ this._scrollCleanupFn?.();
877
+ this._registeredObservers.forEach((obs) => obs.destroy());
878
+ }
879
+ ngOnDestroy() {
880
+ this._cleanupObservers();
881
+ this._resizeCleanupFn?.();
882
+ }
883
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: ArdiumViewportObserverService, deps: [{ token: i0.RendererFactory2 }], target: i0.ɵɵFactoryTarget.Injectable }); }
884
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: ArdiumViewportObserverService, providedIn: 'root' }); }
885
+ }
886
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: ArdiumViewportObserverService, decorators: [{
887
+ type: Injectable,
888
+ args: [{ providedIn: 'root' }]
889
+ }], ctorParameters: () => [{ type: i0.RendererFactory2 }] });
890
+
891
+ /**
892
+ Detects when the user clicks outside a given element.
893
+
894
+ @license
895
+ Shamelessly stolen from [Christian Liebel](https://github.com/chliebel/angular2-click-outside)
896
+ */
897
+ class ArdiumClickOutsideDirective {
898
+ constructor() {
899
+ this._elementRef = inject(ElementRef);
900
+ this.ardClickOutside = output();
901
+ }
902
+ onClick(event, target) {
903
+ if (!target)
904
+ return;
905
+ const hostContainsTarget = this._elementRef.nativeElement.contains(target);
906
+ if (!hostContainsTarget) {
907
+ this.ardClickOutside.emit(event);
908
+ }
909
+ }
910
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: ArdiumClickOutsideDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
911
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.0.5", type: ArdiumClickOutsideDirective, isStandalone: false, selector: "[ardClickOutside]", outputs: { ardClickOutside: "ardClickOutside" }, host: { listeners: { "document:mousedown": "onClick($event,$event.target)", "document:touchstart": "onClick($event,$event.target)" } }, ngImport: i0 }); }
912
+ }
913
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: ArdiumClickOutsideDirective, decorators: [{
914
+ type: Directive,
915
+ args: [{ selector: '[ardClickOutside]', standalone: false }]
916
+ }], propDecorators: { onClick: [{
917
+ type: HostListener,
918
+ args: ['document:mousedown', ['$event', '$event.target']]
919
+ }, {
920
+ type: HostListener,
921
+ args: ['document:touchstart', ['$event', '$event.target']]
922
+ }] } });
923
+
924
+ class ArdiumClickOutsideModule {
925
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: ArdiumClickOutsideModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
926
+ static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.0.5", ngImport: i0, type: ArdiumClickOutsideModule, declarations: [ArdiumClickOutsideDirective], imports: [CommonModule], exports: [ArdiumClickOutsideDirective] }); }
927
+ static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: ArdiumClickOutsideModule, imports: [CommonModule] }); }
928
+ }
929
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: ArdiumClickOutsideModule, decorators: [{
930
+ type: NgModule,
931
+ args: [{
932
+ declarations: [ArdiumClickOutsideDirective],
933
+ imports: [CommonModule],
934
+ exports: [ArdiumClickOutsideDirective],
935
+ }]
936
+ }] });
937
+
938
+ const UNESCAPED_HTML_REGEX = /[&<>"']/g;
939
+ const HTML_ESCAPES = {
940
+ '&': '&amp;',
941
+ '<': '&lt;',
942
+ '>': '&gt;',
943
+ '"': '&quot;',
944
+ "'": '&#39;',
945
+ };
946
+ function escapeHTML(value) {
947
+ if (!value || !UNESCAPED_HTML_REGEX.test(value))
948
+ return value;
949
+ return value.replace(UNESCAPED_HTML_REGEX, (chr) => HTML_ESCAPES[chr]);
950
+ }
951
+
952
+ class ArdiumEscapeHTMLPipe {
953
+ transform(value) {
954
+ return escapeHTML(value);
955
+ }
956
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: ArdiumEscapeHTMLPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
957
+ static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "19.0.5", ngImport: i0, type: ArdiumEscapeHTMLPipe, isStandalone: false, name: "escapeHTML" }); }
958
+ }
959
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: ArdiumEscapeHTMLPipe, decorators: [{
960
+ type: Pipe,
961
+ args: [{
962
+ name: 'escapeHTML',
963
+ standalone: false,
964
+ }]
965
+ }] });
966
+
967
+ class ArdiumEscapeHTMLModule {
968
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: ArdiumEscapeHTMLModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
969
+ static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.0.5", ngImport: i0, type: ArdiumEscapeHTMLModule, declarations: [ArdiumEscapeHTMLPipe], imports: [CommonModule], exports: [ArdiumEscapeHTMLPipe] }); }
970
+ static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: ArdiumEscapeHTMLModule, imports: [CommonModule] }); }
971
+ }
972
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: ArdiumEscapeHTMLModule, decorators: [{
973
+ type: NgModule,
974
+ args: [{
975
+ declarations: [ArdiumEscapeHTMLPipe],
976
+ imports: [CommonModule],
977
+ exports: [ArdiumEscapeHTMLPipe],
978
+ }]
979
+ }] });
980
+
981
+ const _holdDefaults = {
982
+ delay: 500,
983
+ repeat: 1000 / 15,
984
+ allowSpaceKey: false,
985
+ allowEnterKey: false,
986
+ };
987
+ const ARD_HOLD_DEFAULTS = new InjectionToken('ard-hold-defaults', {
988
+ factory: () => ({
989
+ ..._holdDefaults,
990
+ }),
991
+ });
992
+ function provideHoldDefaults(config) {
993
+ return {
994
+ provide: ARD_HOLD_DEFAULTS,
995
+ useValue: { ..._holdDefaults, ...config },
996
+ };
997
+ }
998
+
999
+ /** Coerces a data-bound value (typically a string) to a boolean. */
1000
+ function coerceBooleanProperty(value) {
1001
+ return value != null && `${value}` !== 'false';
1002
+ }
1003
+
1004
+ function coerceNumberProperty(value, fallbackValue = undefined) {
1005
+ return _isNumberValue(value) ? Number(value) : fallbackValue;
1006
+ }
1007
+ /**
1008
+ * Whether the provided value is considered a number.
1009
+ */
1010
+ function _isNumberValue(value) {
1011
+ return !isNaN(parseFloat(value)) && !isNaN(Number(value));
1012
+ }
1013
+
1014
+ /**
1015
+ * Detects when the user clicks-and-holds a given element.
1016
+ */
1017
+ class ArdiumHoldDirective {
1018
+ constructor() {
1019
+ this._DEFAULTS = inject(ARD_HOLD_DEFAULTS);
1020
+ this.ardHold = output();
1021
+ this.ardHoldDisabled = input(false, {
1022
+ transform: (v) => coerceBooleanProperty(v),
1023
+ });
1024
+ this.ardHoldDelay = input(this._DEFAULTS.delay, {
1025
+ transform: (v) => coerceNumberProperty(v, this._DEFAULTS.delay),
1026
+ });
1027
+ this.ardHoldRepeat = input(this._DEFAULTS.repeat, {
1028
+ transform: (v) => coerceNumberProperty(v, this._DEFAULTS.repeat),
1029
+ });
1030
+ this.ardAllowSpaceKey = input(this._DEFAULTS.allowSpaceKey, {
1031
+ transform: (v) => coerceBooleanProperty(v),
1032
+ });
1033
+ this.ardAllowEnterKey = input(this._DEFAULTS.allowEnterKey, {
1034
+ transform: (v) => coerceBooleanProperty(v),
1035
+ });
1036
+ this.interval = null;
1037
+ this.timeout = null;
1038
+ this.isKeyDown = false;
1039
+ effect(() => {
1040
+ if (this.ardHoldDisabled()) {
1041
+ this._clear();
1042
+ }
1043
+ });
1044
+ }
1045
+ onMouseDown() {
1046
+ if (this.ardHoldDisabled())
1047
+ return;
1048
+ this.timeout = setTimeout(() => {
1049
+ this.timeout = null;
1050
+ this.interval = setInterval(() => {
1051
+ this.ardHold.emit();
1052
+ }, this.ardHoldRepeat());
1053
+ }, this.ardHoldDelay());
1054
+ }
1055
+ onMouseUp() {
1056
+ this._clear();
1057
+ }
1058
+ _clear() {
1059
+ if (this.timeout) {
1060
+ clearTimeout(this.timeout);
1061
+ this.timeout = null;
1062
+ return;
1063
+ }
1064
+ if (!this.interval)
1065
+ return;
1066
+ clearInterval(this.interval);
1067
+ this.interval = null;
1068
+ }
1069
+ onKeyDown(event) {
1070
+ if (this.ardAllowEnterKey() && event.code == 'Enter') {
1071
+ event.preventDefault();
1072
+ }
1073
+ if (this.isKeyDown)
1074
+ return;
1075
+ if ((this.ardAllowSpaceKey() && event.code == 'Space') ||
1076
+ (this.ardAllowEnterKey() && event.code == 'Enter')) {
1077
+ this.onMouseDown();
1078
+ this.isKeyDown = true;
1079
+ }
1080
+ }
1081
+ onKeyUp(event) {
1082
+ if ((this.ardAllowSpaceKey() && event.code == 'Space') ||
1083
+ (this.ardAllowEnterKey() && event.code == 'Enter')) {
1084
+ this.onMouseUp();
1085
+ this.isKeyDown = false;
1086
+ }
1087
+ }
1088
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: ArdiumHoldDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1089
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "19.0.5", type: ArdiumHoldDirective, isStandalone: false, selector: "[ardHold]", inputs: { ardHoldDisabled: { classPropertyName: "ardHoldDisabled", publicName: "ardHoldDisabled", isSignal: true, isRequired: false, transformFunction: null }, ardHoldDelay: { classPropertyName: "ardHoldDelay", publicName: "ardHoldDelay", isSignal: true, isRequired: false, transformFunction: null }, ardHoldRepeat: { classPropertyName: "ardHoldRepeat", publicName: "ardHoldRepeat", isSignal: true, isRequired: false, transformFunction: null }, ardAllowSpaceKey: { classPropertyName: "ardAllowSpaceKey", publicName: "ardAllowSpaceKey", isSignal: true, isRequired: false, transformFunction: null }, ardAllowEnterKey: { classPropertyName: "ardAllowEnterKey", publicName: "ardAllowEnterKey", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { ardHold: "ardHold" }, host: { listeners: { "mousedown": "onMouseDown()", "touchstart": "onMouseDown()", "document:mouseup": "onMouseUp()", "document:touchend": "onMouseUp()", "keydown": "onKeyDown($event)", "keyup": "onKeyUp($event)" } }, ngImport: i0 }); }
1090
+ }
1091
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: ArdiumHoldDirective, decorators: [{
1092
+ type: Directive,
1093
+ args: [{ selector: '[ardHold]', standalone: false }]
1094
+ }], ctorParameters: () => [], propDecorators: { onMouseDown: [{
1095
+ type: HostListener,
1096
+ args: ['mousedown']
1097
+ }, {
1098
+ type: HostListener,
1099
+ args: ['touchstart']
1100
+ }], onMouseUp: [{
1101
+ type: HostListener,
1102
+ args: ['document:mouseup']
1103
+ }, {
1104
+ type: HostListener,
1105
+ args: ['document:touchend']
1106
+ }], onKeyDown: [{
1107
+ type: HostListener,
1108
+ args: ['keydown', ['$event']]
1109
+ }], onKeyUp: [{
1110
+ type: HostListener,
1111
+ args: ['keyup', ['$event']]
1112
+ }] } });
1113
+
1114
+ class ArdiumHoldModule {
1115
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: ArdiumHoldModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
1116
+ static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.0.5", ngImport: i0, type: ArdiumHoldModule, declarations: [ArdiumHoldDirective], imports: [CommonModule], exports: [ArdiumHoldDirective] }); }
1117
+ static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: ArdiumHoldModule, imports: [CommonModule] }); }
1118
+ }
1119
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: ArdiumHoldModule, decorators: [{
1120
+ type: NgModule,
1121
+ args: [{
1122
+ declarations: [ArdiumHoldDirective],
1123
+ imports: [CommonModule],
1124
+ exports: [ArdiumHoldDirective],
1125
+ }]
1126
+ }] });
1127
+
1128
+ const ArdInfScrollTarget = {
1129
+ Host: 'host',
1130
+ HTML: 'html',
1131
+ };
1132
+ class ArdiumInfiniteScrollDirective {
1133
+ _getElementRect() {
1134
+ return this.elementRef.nativeElement.getBoundingClientRect();
1135
+ }
1136
+ _getElementScrollTop() {
1137
+ return this.elementRef.nativeElement.scrollTop;
1138
+ }
1139
+ _getElementScrollHeight() {
1140
+ return this.elementRef.nativeElement.scrollHeight;
1141
+ }
1142
+ constructor() {
1143
+ this.elementRef = inject(ElementRef);
1144
+ this._DEFAULTS = inject(ARD_INFINITE_SCROLL_DEFAULTS);
1145
+ this.ardInfScrollReachThreshold = output();
1146
+ //! options
1147
+ this.ardInfScrollThreshold = input(this._DEFAULTS.threshold, {
1148
+ transform: (v) => coerceNumberProperty(v, this._DEFAULTS.threshold),
1149
+ });
1150
+ this.ardInfScrollActive = model(true);
1151
+ this.ardInfScrollTarget = input(this._DEFAULTS.target);
1152
+ // re-check if threshold is reached immediately when directive becomes active again
1153
+ // needed when the content is not enough to fill the container and thus the scroll is already at the end when the directive becomes active
1154
+ effect(() => {
1155
+ const active = this.ardInfScrollActive();
1156
+ if (!active)
1157
+ return;
1158
+ const target = untracked(() => this.ardInfScrollTarget());
1159
+ if (target === ArdInfScrollTarget.Host) {
1160
+ this._detectHostReachThreshold();
1161
+ return;
1162
+ }
1163
+ this._detectWindowReachThreshold();
1164
+ });
1165
+ }
1166
+ //! event handlers
1167
+ onHostScroll() {
1168
+ if (!this.ardInfScrollActive())
1169
+ return;
1170
+ if (this.ardInfScrollTarget() !== ArdInfScrollTarget.Host) {
1171
+ console.error(`DKT-NF1000: ardInfScroll directive has target set to "html", but the host element has just been scrolled. To ensure the directive functions properly, make the host element unscrollable.`);
1172
+ return;
1173
+ }
1174
+ this._detectHostReachThreshold();
1175
+ }
1176
+ onWindowScroll() {
1177
+ if (!this.ardInfScrollActive())
1178
+ return;
1179
+ if (this.ardInfScrollTarget() !== ArdInfScrollTarget.HTML)
1180
+ return;
1181
+ this._detectWindowReachThreshold();
1182
+ }
1183
+ _detectHostReachThreshold() {
1184
+ const { height } = this._getElementRect();
1185
+ const scrollTop = this._getElementScrollTop();
1186
+ const scrollHeight = this._getElementScrollHeight();
1187
+ const thresholdOffset = this.ardInfScrollThreshold();
1188
+ if (scrollTop + height >= scrollHeight - thresholdOffset) {
1189
+ this.ardInfScrollActive.set(false);
1190
+ this.ardInfScrollReachThreshold.emit();
1191
+ }
1192
+ }
1193
+ _detectWindowReachThreshold() {
1194
+ const { top, height } = this._getElementRect();
1195
+ const viewportHeight = window.innerHeight;
1196
+ const thresholdOffset = this.ardInfScrollThreshold();
1197
+ if (-1 * top + viewportHeight >= height - thresholdOffset) {
1198
+ this.ardInfScrollActive.set(false);
1199
+ this.ardInfScrollReachThreshold.emit();
1200
+ }
1201
+ }
1202
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: ArdiumInfiniteScrollDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1203
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "19.0.5", type: ArdiumInfiniteScrollDirective, isStandalone: false, selector: "[ardInfScroll]", inputs: { ardInfScrollThreshold: { classPropertyName: "ardInfScrollThreshold", publicName: "ardInfScrollThreshold", isSignal: true, isRequired: false, transformFunction: null }, ardInfScrollActive: { classPropertyName: "ardInfScrollActive", publicName: "ardInfScrollActive", isSignal: true, isRequired: false, transformFunction: null }, ardInfScrollTarget: { classPropertyName: "ardInfScrollTarget", publicName: "ardInfScrollTarget", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { ardInfScrollReachThreshold: "ardInfScrollReachThreshold", ardInfScrollActive: "ardInfScrollActiveChange" }, host: { listeners: { "scroll": "onHostScroll()", "window:scroll": "onWindowScroll()" } }, ngImport: i0 }); }
1204
+ }
1205
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: ArdiumInfiniteScrollDirective, decorators: [{
1206
+ type: Directive,
1207
+ args: [{
1208
+ selector: '[ardInfScroll]',
1209
+ standalone: false,
1210
+ }]
1211
+ }], ctorParameters: () => [], propDecorators: { onHostScroll: [{
1212
+ type: HostListener,
1213
+ args: ['scroll']
1214
+ }], onWindowScroll: [{
1215
+ type: HostListener,
1216
+ args: ['window:scroll']
1217
+ }] } });
1218
+
1219
+ const _infiniteScrollDefaults = {
1220
+ threshold: 200,
1221
+ target: ArdInfScrollTarget.HTML,
1222
+ };
1223
+ const ARD_INFINITE_SCROLL_DEFAULTS = new InjectionToken('ard-infinite-scroll-defaults', {
1224
+ factory: () => ({ ..._infiniteScrollDefaults }),
1225
+ });
1226
+ function provideInfiniteScrollDefaults(config) {
1227
+ return {
1228
+ provide: ARD_INFINITE_SCROLL_DEFAULTS,
1229
+ useValue: { ..._infiniteScrollDefaults, ...config },
1230
+ };
1231
+ }
1232
+
1233
+ class ArdiumInfiniteScrollModule {
1234
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: ArdiumInfiniteScrollModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
1235
+ static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.0.5", ngImport: i0, type: ArdiumInfiniteScrollModule, declarations: [ArdiumInfiniteScrollDirective], imports: [CommonModule], exports: [ArdiumInfiniteScrollDirective] }); }
1236
+ static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: ArdiumInfiniteScrollModule, imports: [CommonModule] }); }
1237
+ }
1238
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: ArdiumInfiniteScrollModule, decorators: [{
1239
+ type: NgModule,
1240
+ args: [{
1241
+ declarations: [ArdiumInfiniteScrollDirective],
1242
+ imports: [CommonModule],
1243
+ exports: [ArdiumInfiniteScrollDirective],
1244
+ }]
1245
+ }] });
1246
+
1247
+ const _filePipesDefaults = {
1248
+ extensionWithDot: true,
1249
+ sizePrecision: 2,
1250
+ sizeUseSpace: true,
1251
+ };
1252
+ const ARD_FILE_PIPES_DEFAULTS = new InjectionToken('ard-file-pipes-defaults', {
1253
+ factory: () => ({
1254
+ ..._filePipesDefaults,
1255
+ }),
1256
+ });
1257
+ function provideFilePipesDefaults(config) {
1258
+ return {
1259
+ provide: ARD_FILE_PIPES_DEFAULTS,
1260
+ useValue: { ..._filePipesDefaults, ...config },
1261
+ };
1262
+ }
1263
+
1264
+ /**
1265
+ * Formats a file extension from a string or File object.
1266
+ *
1267
+ * @param {string | File | null | undefined} value - The file name or File object whose extension is to be extracted.
1268
+ * @param {boolean} [withDot=false] - Whether to include a dot ('.') before the file extension.
1269
+ * @returns {string | null} - The file extension, or null if no extension is found or if the input is invalid.
1270
+ *
1271
+ * @example
1272
+ * formatFileExtension('image.png'); // -> 'png'
1273
+ * formatFileExtension('image.png', true); // -> '.png'
1274
+ */
1275
+ function formatFileExtension(value, withDot = false) {
1276
+ if (!value)
1277
+ return null;
1278
+ if (typeof value !== 'string') {
1279
+ value = value.name;
1280
+ }
1281
+ const ext = value.match(/^.+\.([^.]+)$/)?.[1] ?? null;
1282
+ const dot = withDot ? '.' : '';
1283
+ return ext ? dot + ext : null;
1284
+ }
1285
+ /**
1286
+ * Pipe that transforms a file or file name into its extension.
1287
+ *
1288
+ * @example
1289
+ * {{ 'image.png' | fileext }} // -> 'png'
1290
+ * {{ 'image.png' | fileext: true }} // -> '.png'
1291
+ */
1292
+ class ArdiumFileExtensionPipe {
1293
+ constructor() {
1294
+ this._DEFAULTS = inject(ARD_FILE_PIPES_DEFAULTS);
1295
+ }
1296
+ /**
1297
+ * Transforms the given value to its file extension.
1298
+ *
1299
+ * @param {string | File | null | undefined} value - The file name or File object to extract the extension from.
1300
+ * @param {boolean} withDot - Whether to include a dot before the extension (defaults to config value).
1301
+ * @returns {string | null} - The file extension or null if no extension is found.
1302
+ *
1303
+ *
1304
+ * @example
1305
+ * {{ 'image.png' | fileext }} // -> 'png'
1306
+ * {{ 'image.png' | fileext: true }} // -> '.png'
1307
+ */
1308
+ transform(value, withDot = this._DEFAULTS.extensionWithDot) {
1309
+ return formatFileExtension(value, withDot);
1310
+ }
1311
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: ArdiumFileExtensionPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
1312
+ static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "19.0.5", ngImport: i0, type: ArdiumFileExtensionPipe, isStandalone: false, name: "fileext" }); }
1313
+ }
1314
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: ArdiumFileExtensionPipe, decorators: [{
1315
+ type: Pipe,
1316
+ args: [{
1317
+ name: 'fileext',
1318
+ standalone: false,
1319
+ }]
1320
+ }] });
1321
+
1322
+ /**
1323
+ * Formats a file name by removing its extension.
1324
+ *
1325
+ * @param {string | File | null | undefined} value - The file name or File object to extract the name without extension from.
1326
+ * @returns {string} - The file name without the extension, or the original input if no extension is found.
1327
+ *
1328
+ * @example
1329
+ * formatFileName('image.png'); // -> 'image'
1330
+ * formatFileName('document.pdf'); // -> 'document'
1331
+ * formatFileName('my-component.component.ts'); // -> 'my-component.component'
1332
+ */
1333
+ function formatFileName(value) {
1334
+ if (!value)
1335
+ return "";
1336
+ if (typeof value != "string") {
1337
+ value = value.name;
1338
+ }
1339
+ return value.match(/^(.+)\.[^.]+$/)?.[1] ?? value;
1340
+ }
1341
+ /**
1342
+ * Pipe that transforms a file or file name into its name without the extension.
1343
+ *
1344
+ * @example
1345
+ * {{ 'image.png' | filename }} // -> 'image'
1346
+ * {{ 'document.pdf' | filename }} // -> 'document'
1347
+ * {{ 'my-component.component.ts' | filename }} // -> 'my-component.component'
1348
+ */
1349
+ class ArdiumFileNamePipe {
1350
+ /**
1351
+ * Transforms the given value to its file name without the extension.
1352
+ *
1353
+ * @param {string | File | null | undefined} value - The file name or File object to extract the name without extension from.
1354
+ * @returns {string} - The file name without the extension.
1355
+ *
1356
+ * @example
1357
+ * {{ 'image.png' | filename }} // -> 'image'
1358
+ * {{ 'document.pdf' | filename }} // -> 'document'
1359
+ * {{ 'my-component.component.ts' | filename }} // -> 'my-component.component'
1360
+ */
1361
+ transform(value) {
1362
+ return formatFileName(value);
1363
+ }
1364
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: ArdiumFileNamePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
1365
+ static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "19.0.5", ngImport: i0, type: ArdiumFileNamePipe, isStandalone: false, name: "filename" }); }
1366
+ }
1367
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: ArdiumFileNamePipe, decorators: [{
1368
+ type: Pipe,
1369
+ args: [{
1370
+ name: "filename",
1371
+ standalone: false,
1372
+ }]
1373
+ }] });
1374
+
1375
+ const FILE_SIZE_UNITS = [
1376
+ { unit: 'B', size: 1 }, // bytes
1377
+ { unit: 'kB', size: 1e3 }, // kilobytes
1378
+ { unit: 'MB', size: 1e6 }, // megabytes
1379
+ { unit: 'GB', size: 1e9 }, // gigabytes
1380
+ { unit: 'TB', size: 1e12 }, // terabytes
1381
+ { unit: 'PB', size: 1e15 }, // petabytes
1382
+ { unit: 'EB', size: 1e18 }, // exabytes
1383
+ { unit: 'ZB', size: 1e21 }, // zettabytes
1384
+ { unit: 'YB', size: 1e24 }, // yottabytes
1385
+ ];
1386
+ /**
1387
+ * Formats a file size into a human-readable string with the appropriate unit.
1388
+ *
1389
+ * @param {number | File | null | undefined} value - The file size as a number or a File object whose size is to be extracted.
1390
+ * @param {number} [precision=2] - The number of decimal places to round to. Default is 2.
1391
+ * @param {boolean} [useSpace=true] - Whether to include a space between the number and the unit. Default is true.
1392
+ * @returns {string} - The formatted file size with the unit (e.g., '1.5 MB', '10 kB').
1393
+ *
1394
+ * @example
1395
+ * formatFileSize(1024); // -> '1.00 kB'
1396
+ * formatFileSize(1048576); // -> '1.00 MB'
1397
+ * formatFileSize(1048576, 3); // -> '1.000 MB'
1398
+ * formatFileSize(1048576, 3, false); // -> '1.000MB'
1399
+ * formatFileSize(967680, 0); // -> '945 kB'
1400
+ */
1401
+ function formatFileSize(value, precision = 2, useSpace = true) {
1402
+ const space = useSpace ? ' ' : '';
1403
+ if (!value)
1404
+ return `0${space}B`;
1405
+ if (typeof value != 'number') {
1406
+ value = value.size;
1407
+ }
1408
+ if (value === 0)
1409
+ return `0${space}B`;
1410
+ const index = FILE_SIZE_UNITS.findIndex((unit) => value < unit.size);
1411
+ const fileSizeUnit = index === -1
1412
+ ? FILE_SIZE_UNITS[FILE_SIZE_UNITS.length - 1]
1413
+ : FILE_SIZE_UNITS[index - 1];
1414
+ const convertedValue = fileSizeUnit.unit === 'B'
1415
+ ? value
1416
+ : (value / fileSizeUnit.size).toFixed(precision);
1417
+ return `${convertedValue}${space}${fileSizeUnit.unit}`;
1418
+ }
1419
+ /**
1420
+ * Pipe that transforms a file or file size into a human-readable file size string.
1421
+ *
1422
+ * @example
1423
+ * {{ 1024 | filesize }} // -> '1.00 kB'
1424
+ * {{ 1048576 | filesize }} // -> '1.00 MB'
1425
+ * {{ 1048576 | filesize:3 }} // -> '1.000 MB'
1426
+ * {{ 1048576 | filesize:3:false }} // -> '1.000MB'
1427
+ * {{ 967680 | filesize:0 }} // -> '945 kB'
1428
+ */
1429
+ class ArdiumFileSizePipe {
1430
+ constructor() {
1431
+ /** Default configuration for the pipe */
1432
+ this._DEFAULTS = inject(ARD_FILE_PIPES_DEFAULTS);
1433
+ }
1434
+ /**
1435
+ * Transforms the given value to a human-readable file size string.
1436
+ *
1437
+ * @param {number | File | null | undefined} value - The file size as a number or File object whose size is to be extracted.
1438
+ * @param {number} precision - The number of decimal places to round to (defaults to config value).
1439
+ * @param {boolean} useSpace - Whether to include a space between the number and the unit (defaults to config value).
1440
+ * @returns {string} - The formatted file size string (e.g., '1.5 MB', '10 kB').
1441
+ *
1442
+ * @example
1443
+ * {{ 1024 | filesize }} // -> '1.00 kB'
1444
+ * {{ 1048576 | filesize }} // -> '1.00 MB'
1445
+ * {{ 1048576 | filesize:3 }} // -> '1.000 MB'
1446
+ * {{ 1048576 | filesize:3:false }} // -> '1.000MB'
1447
+ * {{ 967680 | filesize:0 }} // -> '945 kB'
1448
+ */
1449
+ transform(value, precision = this._DEFAULTS.sizePrecision, useSpace = this._DEFAULTS.sizeUseSpace) {
1450
+ return formatFileSize(value, precision, useSpace);
1451
+ }
1452
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: ArdiumFileSizePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
1453
+ static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "19.0.5", ngImport: i0, type: ArdiumFileSizePipe, isStandalone: false, name: "filesize" }); }
1454
+ }
1455
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: ArdiumFileSizePipe, decorators: [{
1456
+ type: Pipe,
1457
+ args: [{
1458
+ name: 'filesize',
1459
+ standalone: false,
1460
+ }]
1461
+ }] });
1462
+
1463
+ class ArdiumFilePipesModule {
1464
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: ArdiumFilePipesModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
1465
+ static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.0.5", ngImport: i0, type: ArdiumFilePipesModule, declarations: [ArdiumFileExtensionPipe,
1466
+ ArdiumFileNamePipe,
1467
+ ArdiumFileSizePipe], imports: [CommonModule], exports: [ArdiumFileExtensionPipe, ArdiumFileNamePipe, ArdiumFileSizePipe] }); }
1468
+ static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: ArdiumFilePipesModule, imports: [CommonModule] }); }
1469
+ }
1470
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: ArdiumFilePipesModule, decorators: [{
1471
+ type: NgModule,
1472
+ args: [{
1473
+ declarations: [
1474
+ ArdiumFileExtensionPipe,
1475
+ ArdiumFileNamePipe,
1476
+ ArdiumFileSizePipe,
1477
+ ],
1478
+ imports: [CommonModule],
1479
+ exports: [ArdiumFileExtensionPipe, ArdiumFileNamePipe, ArdiumFileSizePipe],
1480
+ }]
1481
+ }] });
1482
+
1483
+ /**
1484
+ * Creates a writable, reactive array signal with convenient, immutable array manipulation methods.
1485
+ *
1486
+ * This helper returns a signal that wraps an array and exposes a full suite of mutating and non-mutating array methods,
1487
+ * all of which will automatically trigger Angular reactivity.
1488
+ *
1489
+ * All mutator methods (such as `push`, `pop`, `splice`, `setAt`, `updateAt`, etc) return a new array value, preserving
1490
+ * immutability for optimal signal reactivity. Do not depend on reference equality between updates.
1491
+ *
1492
+ * @template T The type of items stored in the array.
1493
+ * @param {T[]} [initialValue=[]] Optional initial value for the array.
1494
+ * @returns {WritableArraySignal<T>} A writable array signal with both native and array-specific helpers.
1495
+ *
1496
+ * @example
1497
+ * const numbers = arraySignal([1, 2, 3]);
1498
+ * numbers.push(4); // [1, 2, 3, 4]
1499
+ * numbers.pop(); // returns 4, numbers() === [1, 2, 3]
1500
+ * numbers.setAt(1, 99); // [1, 99, 3]
1501
+ * numbers.filter(x => x>1); // [99, 3]
1502
+ */
1503
+ function arraySignal(initialValue = []) {
1504
+ const internalSignal = signal([
1505
+ ...initialValue,
1506
+ ]);
1507
+ const _set = internalSignal.set;
1508
+ const _update = internalSignal.update;
1509
+ const _asReadonly = internalSignal.asReadonly;
1510
+ // Native signal methods
1511
+ internalSignal.set = (value) => _set([...value]);
1512
+ internalSignal.update = (updateFn) => {
1513
+ _update((current) => [...updateFn(current)]);
1514
+ };
1515
+ // isEmpty
1516
+ internalSignal.isEmpty = computed(() => internalSignal().length === 0);
1517
+ // setAt & updateAt
1518
+ internalSignal.setAt = (index, newItem) => {
1519
+ internalSignal.update((arr) => {
1520
+ const newArr = [...arr];
1521
+ newArr[index] = newItem;
1522
+ return newArr;
1523
+ });
1524
+ };
1525
+ internalSignal.updateAt = (index, updateFn) => {
1526
+ internalSignal.update((arr) => {
1527
+ const newArr = [...arr];
1528
+ newArr[index] = updateFn(newArr[index]);
1529
+ return newArr;
1530
+ });
1531
+ };
1532
+ // Array methods
1533
+ internalSignal.fill = (value, start, end) => {
1534
+ internalSignal.update((arr) => {
1535
+ const newArr = [...arr];
1536
+ newArr.fill(value, start, end);
1537
+ return newArr;
1538
+ });
1539
+ };
1540
+ internalSignal.filter = (predicate) => {
1541
+ internalSignal.update((arr) => arr.filter(predicate));
1542
+ };
1543
+ internalSignal.map = (mapFn) => {
1544
+ internalSignal.update((arr) => arr.map(mapFn));
1545
+ };
1546
+ internalSignal.pop = () => {
1547
+ let value;
1548
+ internalSignal.update((arr) => {
1549
+ const newArr = [...arr];
1550
+ value = newArr.pop();
1551
+ return newArr;
1552
+ });
1553
+ return value;
1554
+ };
1555
+ internalSignal.push = (...items) => {
1556
+ let length;
1557
+ internalSignal.update((arr) => {
1558
+ const newArr = [...arr, ...items];
1559
+ length = newArr.length;
1560
+ return newArr;
1561
+ });
1562
+ return length;
1563
+ };
1564
+ internalSignal.reverse = () => {
1565
+ internalSignal.update((arr) => {
1566
+ const newArr = [...arr];
1567
+ newArr.reverse();
1568
+ return newArr;
1569
+ });
1570
+ };
1571
+ internalSignal.shift = () => {
1572
+ let value;
1573
+ internalSignal.update((arr) => {
1574
+ const newArr = [...arr];
1575
+ value = newArr.shift();
1576
+ return newArr;
1577
+ });
1578
+ return value;
1579
+ };
1580
+ internalSignal.sort = (compareFn) => {
1581
+ internalSignal.update((arr) => {
1582
+ const newArr = [...arr];
1583
+ newArr.sort(compareFn);
1584
+ return newArr;
1585
+ });
1586
+ };
1587
+ internalSignal.splice = (start, deleteCount, ...items) => {
1588
+ let removed = [];
1589
+ internalSignal.update((arr) => {
1590
+ const newArr = [...arr];
1591
+ removed = newArr.splice(start, deleteCount ?? arr.length, ...items);
1592
+ return newArr;
1593
+ });
1594
+ return removed;
1595
+ };
1596
+ internalSignal.unshift = (...items) => {
1597
+ let length;
1598
+ internalSignal.update((arr) => {
1599
+ const newArr = [...items, ...arr];
1600
+ length = newArr.length;
1601
+ return newArr;
1602
+ });
1603
+ return length;
1604
+ };
1605
+ // create a wrapper with only
1606
+ internalSignal.asReadonly = () => {
1607
+ const readonlySignal = _asReadonly();
1608
+ readonlySignal.isEmpty = internalSignal.isEmpty;
1609
+ return readonlySignal;
1610
+ };
1611
+ return internalSignal;
1612
+ }
1613
+
1614
+ /**
1615
+ * Creates a writable, reactive counter signal with convenient helpers and computed queries.
1616
+ *
1617
+ * @param {number} [initial=0] The initial value of the counter.
1618
+ * @returns {WritableCounterSignal} A writable counter signal instance.
1619
+ *
1620
+ * @example
1621
+ * const count = counterSignal(10);
1622
+ * count.inc(); // 11
1623
+ * count.dec(2); // 9
1624
+ * count.reset(); // 10
1625
+ * count.isReset(); // true
1626
+ */
1627
+ function counterSignal(initial = 0) {
1628
+ const internalSignal = signal(initial);
1629
+ const isResetSignal = signal(true);
1630
+ const _set = internalSignal.set;
1631
+ const _update = internalSignal.update;
1632
+ const _asReadonly = internalSignal.asReadonly;
1633
+ let _initial = initial;
1634
+ // Native methods
1635
+ internalSignal.set = (value) => {
1636
+ _set(value);
1637
+ isResetSignal.set(false);
1638
+ };
1639
+ internalSignal.update = (updater) => {
1640
+ _update((current) => updater(current));
1641
+ isResetSignal.set(false);
1642
+ };
1643
+ // Counter methods
1644
+ internalSignal.increment = (amount = 1) => {
1645
+ internalSignal.update((val) => val + amount);
1646
+ };
1647
+ internalSignal.decrement = (amount = 1) => {
1648
+ internalSignal.update((val) => val - amount);
1649
+ };
1650
+ internalSignal.reset = () => {
1651
+ _set(_initial);
1652
+ isResetSignal.set(true);
1653
+ };
1654
+ internalSignal.setBaseValue = (value) => {
1655
+ _initial = value;
1656
+ };
1657
+ internalSignal.asReadonly = () => {
1658
+ const readonly = _asReadonly();
1659
+ readonly.isReset = internalSignal.isReset;
1660
+ readonly.value = internalSignal.value;
1661
+ return readonly;
1662
+ };
1663
+ internalSignal.value = internalSignal.asReadonly();
1664
+ internalSignal.isReset = isResetSignal.asReadonly();
1665
+ return internalSignal;
1666
+ }
1667
+
1668
+ /**
1669
+ * Creates a `WritableSignal` that delays updates to its value until there's a pause in updates, based on the debounce time specified.
1670
+ *
1671
+ * @param initialValue - The initial value of the signal.
1672
+ * @param debounceTime - The delay in milliseconds after the last update before applying the signal update.
1673
+ * @returns A `WritableSignal<T>` that debounces value updates based on the specified debounce time.
1674
+ */
1675
+ function debouncedSignal(initialValue, debounceTime) {
1676
+ const internalSignal = signal(initialValue);
1677
+ Object.assign(internalSignal, { debounceTime });
1678
+ let timeoutId = null;
1679
+ const originalSet = internalSignal.set;
1680
+ const setDebouncedValue = (value) => {
1681
+ // Clear the previous timeout if a new value is set before the debounce period ends
1682
+ if (timeoutId) {
1683
+ clearTimeout(timeoutId);
1684
+ }
1685
+ // Set a new timeout to update the signal after the debounce period
1686
+ timeoutId = setTimeout(() => {
1687
+ originalSet(value);
1688
+ timeoutId = null;
1689
+ }, debounceTime);
1690
+ };
1691
+ // Override the `set` method to include debouncing
1692
+ internalSignal.set = (value) => {
1693
+ setDebouncedValue(value);
1694
+ };
1695
+ internalSignal.update = (fn) => {
1696
+ setDebouncedValue(fn(internalSignal()));
1697
+ };
1698
+ return internalSignal;
1699
+ }
1700
+
1701
+ /**
1702
+ * Creates a writable, reactive Map signal with full Map-like API and helpers.
1703
+ *
1704
+ * This factory returns a signal wrapping a JavaScript `Map`, exposing both non-mutating and mutating methods
1705
+ * for convenient and fully reactive manipulation of key-value pairs.
1706
+ *
1707
+ * All mutator methods (`setKey`, `delete`, `clear`, `setMap`, etc.) always replace the Map with a cloned version,
1708
+ * ensuring immutability for predictable reactivity.
1709
+ *
1710
+ * The signal also provides computed signals for common queries (such as `isEmpty`, `size`, `entriesArray`, `keysArray`, `valuesArray`),
1711
+ * and offers a type-safe, readonly view via `.asReadonly()`.
1712
+ *
1713
+ * @template K The type of keys.
1714
+ * @template V The type of values.
1715
+ * @param {Iterable<[K, V]>} [initialValue=[]] Optional initial map contents, as entries or another Map.
1716
+ * @returns {WritableMapSignal<K, V>} A writable Map signal instance with both native and Map-specific helpers.
1717
+ *
1718
+ * @example
1719
+ * const map = mapSignal<string, number>([['a', 1], ['b', 2]]);
1720
+ * map.setKey('c', 3); // Adds key 'c'
1721
+ * map.delete('a'); // Removes key 'a'
1722
+ * map.update(m => { m.set('d', 4); return m; });
1723
+ * map.entriesArray(); // [['b', 2], ['c', 3], ['d', 4]]
1724
+ */
1725
+ function mapSignal(initialValue = []) {
1726
+ const internalSignal = signal(new Map(initialValue));
1727
+ const _set = internalSignal.set;
1728
+ const _update = internalSignal.update;
1729
+ const _asReadonly = internalSignal.asReadonly;
1730
+ internalSignal.setMap = (value) => _set(new Map(value));
1731
+ internalSignal.set = (value) => _set(new Map(value));
1732
+ internalSignal.setKey = (key, value) => {
1733
+ internalSignal.update((map) => {
1734
+ const copy = new Map(map);
1735
+ copy.set(key, value);
1736
+ return copy;
1737
+ });
1738
+ };
1739
+ internalSignal.delete = (key) => {
1740
+ let deleted = false;
1741
+ internalSignal.update((map) => {
1742
+ if (!map.has(key))
1743
+ return map;
1744
+ const copy = new Map(map);
1745
+ deleted = copy.delete(key);
1746
+ return copy;
1747
+ });
1748
+ return deleted;
1749
+ };
1750
+ internalSignal.clear = () => {
1751
+ internalSignal.setMap(new Map());
1752
+ };
1753
+ internalSignal.get = (key) => internalSignal().get(key);
1754
+ internalSignal.has = (key) => internalSignal().has(key);
1755
+ internalSignal.update = (updateFn) => {
1756
+ _update((current) => new Map(updateFn(current)));
1757
+ };
1758
+ internalSignal.updateAt = (key, updateFn) => {
1759
+ internalSignal.update((map) => {
1760
+ if (!map.has(key))
1761
+ return map;
1762
+ const copy = new Map(map);
1763
+ copy.set(key, updateFn(copy.get(key)));
1764
+ return copy;
1765
+ });
1766
+ };
1767
+ internalSignal.entriesArray = computed(() => Array.from(internalSignal().entries()));
1768
+ internalSignal.keysArray = computed(() => Array.from(internalSignal().keys()));
1769
+ internalSignal.valuesArray = computed(() => Array.from(internalSignal().values()));
1770
+ internalSignal.isEmpty = computed(() => internalSignal().size === 0);
1771
+ internalSignal.size = computed(() => internalSignal().size);
1772
+ internalSignal.asReadonly = () => {
1773
+ const readonlySignal = _asReadonly();
1774
+ readonlySignal.isEmpty = internalSignal.isEmpty;
1775
+ readonlySignal.size = internalSignal.size;
1776
+ readonlySignal.get = internalSignal.get;
1777
+ readonlySignal.has = internalSignal.has;
1778
+ readonlySignal.entriesArray = internalSignal.entriesArray;
1779
+ readonlySignal.keysArray = internalSignal.keysArray;
1780
+ readonlySignal.valuesArray = internalSignal.valuesArray;
1781
+ return readonlySignal;
1782
+ };
1783
+ return internalSignal;
1784
+ }
1785
+
1786
+ /**
1787
+ * Defines the available persistent storage methods for the `persistentSignal`. Each method corresponds to a different browser storage mechanism:
1788
+ * - `PersistentStorageMethod.LocalStorage`: Uses the browser's localStorage to persist the value across sessions.
1789
+ * - `PersistentStorageMethod.SessionStorage`: Uses the browser's sessionStorage to persist the value for the duration of the page session.
1790
+ * - `PersistentStorageMethod.Cookies`: Uses browser cookies to persist the value, allowing for additional options like expiration and path.
1791
+ */
1792
+ const PersistentStorageMethod = {
1793
+ Cookies: 'cookies',
1794
+ LocalStorage: 'local-storage',
1795
+ SessionStorage: 'session-storage',
1796
+ };
1797
+ function isSerializableSignal$1(options) {
1798
+ return !!options.serialize && !!options.deserialize;
1799
+ }
1800
+ function persistentSignal(initialValue, options) {
1801
+ if (!!options.serialize !== !!options.deserialize) {
1802
+ throw new Error('DKT-FT3000: Both serialize and deserialize must either be both defined or both undefined.');
1803
+ }
1804
+ const isNonNullable = options.nonNullable === true;
1805
+ if (isNonNullable && isNull(initialValue)) {
1806
+ throw new Error('DKT-FT3002: Non-nullable persistent signals require a non-null initial value.');
1807
+ }
1808
+ if (!isSerializableSignal$1(options) &&
1809
+ !isAnyString(initialValue) &&
1810
+ !isNull(initialValue)) {
1811
+ throw new Error('DKT-FT3001: Non-string initial values are only allowed for serializable signals. Define serialization options.');
1812
+ }
1813
+ const internalSignal = signal(initialValue);
1814
+ const _asReadonly = internalSignal.asReadonly;
1815
+ if (isNonNullable) {
1816
+ const nonNullableSignal = internalSignal;
1817
+ Object.assign(nonNullableSignal, {
1818
+ method: options.method,
1819
+ key: getKey(options),
1820
+ serialized: computed(() => serializeValue$1(nonNullableSignal(), options)),
1821
+ });
1822
+ _assignPersistentSignalListeners(nonNullableSignal, options);
1823
+ nonNullableSignal.asReadonly = () => {
1824
+ const readonlySignal = _asReadonly();
1825
+ readonlySignal.method = nonNullableSignal.method;
1826
+ readonlySignal.key = nonNullableSignal.key;
1827
+ readonlySignal.serialized = nonNullableSignal.serialized;
1828
+ return readonlySignal;
1829
+ };
1830
+ return nonNullableSignal;
1831
+ }
1832
+ const nullableSignal = internalSignal;
1833
+ Object.assign(nullableSignal, {
1834
+ method: options.method,
1835
+ key: getKey(options),
1836
+ serialized: computed(() => serializeValue$1(nullableSignal(), options)),
1837
+ clear: () => nullableSignal.set(null),
1838
+ });
1839
+ _assignPersistentSignalListeners(nullableSignal, options);
1840
+ nullableSignal.asReadonly = () => {
1841
+ const readonlySignal = _asReadonly();
1842
+ readonlySignal.method = nullableSignal.method;
1843
+ readonlySignal.key = nullableSignal.key;
1844
+ readonlySignal.serialized = nullableSignal.serialized;
1845
+ return readonlySignal;
1846
+ };
1847
+ return nullableSignal;
1848
+ }
1849
+ function _assignPersistentSignalListeners(signal, options) {
1850
+ const storedValue = loadFromStorage(options);
1851
+ if (storedValue !== null) {
1852
+ signal.set(storedValue);
1853
+ }
1854
+ else {
1855
+ updateStorage(options, signal());
1856
+ }
1857
+ const injector = inject(Injector);
1858
+ runInInjectionContext(injector, () => {
1859
+ effect(() => {
1860
+ const value = signal();
1861
+ updateStorage(options, value);
1862
+ });
1863
+ });
1864
+ }
1865
+ function loadFromStorage(options) {
1866
+ let storedValue = null;
1867
+ if (options.method === PersistentStorageMethod.LocalStorage) {
1868
+ storedValue = localStorage.getItem(getKey(options));
1869
+ }
1870
+ else if (options.method === PersistentStorageMethod.SessionStorage) {
1871
+ storedValue = sessionStorage.getItem(getKey(options));
1872
+ }
1873
+ else if (options.method === PersistentStorageMethod.Cookies) {
1874
+ const match = document.cookie.match('(^|;)\\s*' + getKey(options) + '\\s*=\\s*([^;]+)');
1875
+ storedValue = match ? decodeURIComponent(match[2]) : null;
1876
+ }
1877
+ return deserializeValue$1(storedValue, options);
1878
+ }
1879
+ function updateStorage(options, value) {
1880
+ const serializedValue = serializeValue$1(value, options);
1881
+ if (options.method === PersistentStorageMethod.LocalStorage) {
1882
+ if (serializedValue === null)
1883
+ localStorage.removeItem(getKey(options));
1884
+ else
1885
+ localStorage.setItem(getKey(options), serializedValue);
1886
+ }
1887
+ else if (options.method === PersistentStorageMethod.SessionStorage) {
1888
+ if (serializedValue === null)
1889
+ sessionStorage.removeItem(getKey(options));
1890
+ else
1891
+ sessionStorage.setItem(getKey(options), serializedValue);
1892
+ }
1893
+ else if (options.method === PersistentStorageMethod.Cookies) {
1894
+ let cookieString = `${getKey(options)}=${serializedValue !== null ? encodeURIComponent(serializedValue) : ''}`;
1895
+ if (serializedValue !== null) {
1896
+ cookieString += `; expires=Thu, 01 Jan 1970 00:00:01 GMT;`;
1897
+ }
1898
+ else if (options.expires) {
1899
+ const expires = options.expires instanceof Date
1900
+ ? options.expires.toUTCString()
1901
+ : options.expires;
1902
+ cookieString += `; expires=${expires}`;
1903
+ }
1904
+ if (options.maxAge) {
1905
+ cookieString += `; max-age=${options.maxAge}`;
1906
+ }
1907
+ cookieString += `; path=${options.path ?? '/'}`;
1908
+ document.cookie = cookieString;
1909
+ }
1910
+ }
1911
+ function getKey(options) {
1912
+ return 'key' in options ? options.key : options.name;
1913
+ }
1914
+ function serializeValue$1(value, options) {
1915
+ if (isSerializableSignal$1(options)) {
1916
+ return options.serialize(value);
1917
+ }
1918
+ return value === null ? null : value;
1919
+ }
1920
+ function deserializeValue$1(value, options) {
1921
+ if (value !== null && isSerializableSignal$1(options)) {
1922
+ return options.deserialize(value);
1923
+ }
1924
+ return value;
1925
+ }
1926
+
1927
+ function isSerializableSignal(options) {
1928
+ return !!options.serialize && !!options.deserialize;
1929
+ }
1930
+ function queryParamSignal(initialValue, optionsOrParam) {
1931
+ let options;
1932
+ if (typeof optionsOrParam === 'string') {
1933
+ options = { paramName: optionsOrParam };
1934
+ }
1935
+ else {
1936
+ options = optionsOrParam;
1937
+ }
1938
+ if (!!options.serialize !== !!options.deserialize) {
1939
+ throw new Error('DKT-FT3010: Both serialize and deserialize must either be both defined or both undefined.');
1940
+ }
1941
+ const isNonNullable = options.nonNullable === true;
1942
+ if (isNonNullable && isNull(initialValue)) {
1943
+ throw new Error('DKT-FT3012: Non-nullable query param signals require a non-null initial value.');
1944
+ }
1945
+ if (!isSerializableSignal(options) &&
1946
+ !isAnyString(initialValue) &&
1947
+ !isNull(initialValue)) {
1948
+ throw new Error('DKT-FT3011: Non-string initial values are only allowed for serializable signals. Define serialization options.');
1949
+ }
1950
+ const internalSignal = signal(initialValue);
1951
+ const _asReadonly = internalSignal.asReadonly;
1952
+ if (isNonNullable) {
1953
+ const nonNullableSignal = internalSignal;
1954
+ Object.assign(nonNullableSignal, {
1955
+ paramName: options.paramName,
1956
+ serialized: computed(() => serializeValue(nonNullableSignal(), options)),
1957
+ });
1958
+ _assignQueryParamListeners(nonNullableSignal, initialValue, options);
1959
+ nonNullableSignal.asReadonly = () => {
1960
+ const readonlySignal = _asReadonly();
1961
+ readonlySignal.serialized = nonNullableSignal.serialized;
1962
+ return readonlySignal;
1963
+ };
1964
+ return nonNullableSignal;
1965
+ }
1966
+ const nullableSignal = internalSignal;
1967
+ Object.assign(nullableSignal, {
1968
+ paramName: options.paramName,
1969
+ serialized: computed(() => serializeValue(nullableSignal(), options)),
1970
+ clear: () => nullableSignal.set(null),
1971
+ });
1972
+ _assignQueryParamListeners(nullableSignal, initialValue, options);
1973
+ nullableSignal.asReadonly = () => {
1974
+ const readonlySignal = _asReadonly();
1975
+ readonlySignal.serialized = nullableSignal.serialized;
1976
+ return readonlySignal;
1977
+ };
1978
+ return nullableSignal;
1979
+ }
1980
+ function _assignQueryParamListeners(signal, initialValue, options) {
1981
+ const router = inject(Router);
1982
+ const storedValue = loadFromQueryParam(router, options);
1983
+ const destroyRef = inject(DestroyRef);
1984
+ const injector = inject(Injector);
1985
+ let isApplyingRouterValue = false;
1986
+ if (storedValue !== null) {
1987
+ signal.set(storedValue);
1988
+ }
1989
+ else {
1990
+ updateQueryParam(router, options, signal());
1991
+ }
1992
+ setTimeout(() => {
1993
+ updateQueryParam(router, options, signal());
1994
+ }, 0);
1995
+ router.events
1996
+ .pipe(filter((event) => event instanceof NavigationEnd), takeUntilDestroyed(destroyRef))
1997
+ .subscribe(() => {
1998
+ const nextValue = loadFromQueryParam(router, options);
1999
+ const writableSignal = signal;
2000
+ if (options.nonNullable === true && nextValue === null) {
2001
+ writableSignal.set(initialValue);
2002
+ return;
2003
+ }
2004
+ const currentSerialized = serializeValue(writableSignal(), options);
2005
+ const nextSerialized = serializeValue(nextValue, options);
2006
+ if (currentSerialized === nextSerialized) {
2007
+ return;
2008
+ }
2009
+ isApplyingRouterValue = true;
2010
+ writableSignal.set(nextValue);
2011
+ isApplyingRouterValue = false;
2012
+ });
2013
+ runInInjectionContext(injector, () => {
2014
+ effect(() => {
2015
+ if (isApplyingRouterValue) {
2016
+ return;
2017
+ }
2018
+ updateQueryParam(router, options, signal());
2019
+ });
2020
+ });
2021
+ }
2022
+ function loadFromQueryParam(router, options) {
2023
+ const urlTree = router.parseUrl(router.url);
2024
+ const queryParamValue = urlTree.queryParams[options.paramName];
2025
+ const storedValue = queryParamValue === undefined || queryParamValue === null
2026
+ ? null
2027
+ : String(queryParamValue);
2028
+ return deserializeValue(storedValue, options);
2029
+ }
2030
+ function updateQueryParam(router, options, value) {
2031
+ const serializedValue = serializeValue(value, options);
2032
+ const urlTree = router.parseUrl(router.url);
2033
+ const currentParam = urlTree.queryParams[options.paramName] == null
2034
+ ? null
2035
+ : String(urlTree.queryParams[options.paramName]);
2036
+ if (currentParam === serializedValue) {
2037
+ return;
2038
+ }
2039
+ const queryParams = {
2040
+ ...urlTree.queryParams,
2041
+ [options.paramName]: serializedValue,
2042
+ };
2043
+ router.navigate([], {
2044
+ queryParams,
2045
+ queryParamsHandling: 'merge',
2046
+ replaceUrl: true,
2047
+ });
2048
+ }
2049
+ function serializeValue(value, options) {
2050
+ if (isSerializableSignal(options)) {
2051
+ return options.serialize(value);
2052
+ }
2053
+ return value === null ? null : value;
2054
+ }
2055
+ function deserializeValue(value, options) {
2056
+ if (value !== null && isSerializableSignal(options)) {
2057
+ return options.deserialize(value);
2058
+ }
2059
+ return value;
2060
+ }
2061
+
2062
+ /**
2063
+ * Creates a writable queue signal, which is a reactive queue data structure.
2064
+ *
2065
+ * @template T The type of elements held in the queue.
2066
+ * @param {T[]} [initialValue=[]] Optional initial items in the queue.
2067
+ * @returns {WritableQueueSignal<T>} The writable queue signal instance.
2068
+ *
2069
+ * @example
2070
+ * const queue = queueSignal<number>([1, 2, 3]);
2071
+ * queue.enqueue(4); // new value: [1, 2, 3, 4]
2072
+ * const value = queue.dequeue(); // 1 new value: [2, 3, 4]
2073
+ * queue.clear(); // new value: []
2074
+ */
2075
+ function queueSignal(initialValue = []) {
2076
+ const internalSignal = signal([
2077
+ ...initialValue,
2078
+ ]);
2079
+ const _set = internalSignal.set;
2080
+ const _update = internalSignal.update;
2081
+ const _asReadonly = internalSignal.asReadonly;
2082
+ // Native methods
2083
+ internalSignal.set = (arr) => _set([...arr]);
2084
+ internalSignal.update = (updateFn) => {
2085
+ _update((current) => [...updateFn(current)]);
2086
+ };
2087
+ // Queue methods
2088
+ internalSignal.enqueue = (...items) => {
2089
+ internalSignal.update((queue) => [...queue, ...items]);
2090
+ };
2091
+ internalSignal.dequeue = () => {
2092
+ let dequeued = undefined;
2093
+ internalSignal.update((queue) => {
2094
+ if (queue.length === 0)
2095
+ return queue;
2096
+ const copy = [...queue];
2097
+ dequeued = copy.shift();
2098
+ return copy;
2099
+ });
2100
+ return dequeued;
2101
+ };
2102
+ internalSignal.clear = () => {
2103
+ internalSignal.set([]);
2104
+ };
2105
+ internalSignal.isEmpty = computed(() => internalSignal().length === 0);
2106
+ internalSignal.size = computed(() => internalSignal().length);
2107
+ internalSignal.front = computed(() => {
2108
+ const queue = internalSignal();
2109
+ return queue.length ? queue[0] : undefined;
2110
+ });
2111
+ internalSignal.asArray = computed(() => [...internalSignal()]);
2112
+ internalSignal.peek = () => {
2113
+ const queue = internalSignal();
2114
+ return queue.length ? queue[0] : undefined;
2115
+ };
2116
+ internalSignal.asReadonly = () => {
2117
+ const readonlySignal = _asReadonly();
2118
+ readonlySignal.isEmpty = internalSignal.isEmpty;
2119
+ readonlySignal.size = internalSignal.size;
2120
+ readonlySignal.front = internalSignal.front;
2121
+ readonlySignal.asArray = internalSignal.asArray;
2122
+ readonlySignal.peek = internalSignal.peek;
2123
+ return readonlySignal;
2124
+ };
2125
+ return internalSignal;
2126
+ }
2127
+
2128
+ /**
2129
+ * Creates a writable, reactive Set signal with convenient, immutable Set manipulation methods.
2130
+ *
2131
+ * This function returns a signal that wraps a JavaScript `Set` and exposes both non-mutating and mutating helpers,
2132
+ * ensuring that all operations are fully reactive and immutably safe. All mutator methods (`add`, `delete`, `clear`, etc.)
2133
+ * return a new Set value for robust signal reactivity.
2134
+ *
2135
+ * Also provides computed signals for queries such as `isEmpty`, `size`, and `asArray`, and offers a type-safe, read-only
2136
+ * view via `.asReadonly()`.
2137
+ *
2138
+ * @template T The type of items stored in the set.
2139
+ * @param {Iterable<T>} [initialValue=[]] Optional initial values for the set (can be another Set or any iterable).
2140
+ * @returns {WritableSetSignal<T>} A writable Set signal instance with both native and Set-specific helpers.
2141
+ *
2142
+ * @example
2143
+ * const ids = setSignal<number>([1, 2, 3]);
2144
+ * ids.add(4); // [1, 2, 3, 4]
2145
+ * ids.delete(2); // removes 2
2146
+ * ids.has(3); // true
2147
+ * ids.asArray(); // [1, 3, 4]
2148
+ * ids.clear(); // empty set
2149
+ * ids.isEmpty(); // true
2150
+ */
2151
+ function setSignal(initialValue = []) {
2152
+ const internalSignal = signal(new Set(initialValue));
2153
+ const _set = internalSignal.set;
2154
+ const _update = internalSignal.update;
2155
+ const _asReadonly = internalSignal.asReadonly;
2156
+ // Native signal methods
2157
+ internalSignal.set = (value) => _set(new Set(value));
2158
+ internalSignal.update = (updateFn) => {
2159
+ _update((current) => new Set(updateFn(current)));
2160
+ };
2161
+ // Computed helpers
2162
+ internalSignal.isEmpty = computed(() => internalSignal().size === 0);
2163
+ internalSignal.size = computed(() => internalSignal().size);
2164
+ // Set methods
2165
+ internalSignal.add = (value) => {
2166
+ internalSignal.update((set) => {
2167
+ if (set.has(value))
2168
+ return set;
2169
+ const copy = new Set(set);
2170
+ copy.add(value);
2171
+ return copy;
2172
+ });
2173
+ };
2174
+ internalSignal.delete = (value) => {
2175
+ let deleted = false;
2176
+ internalSignal.update((set) => {
2177
+ if (!set.has(value))
2178
+ return set;
2179
+ const copy = new Set(set);
2180
+ deleted = copy.delete(value);
2181
+ return copy;
2182
+ });
2183
+ return deleted;
2184
+ };
2185
+ internalSignal.clear = () => {
2186
+ internalSignal.set(new Set());
2187
+ };
2188
+ internalSignal.has = (value) => {
2189
+ return internalSignal().has(value);
2190
+ };
2191
+ internalSignal.asArray = computed(() => Array.from(internalSignal()));
2192
+ internalSignal.asReadonly = () => {
2193
+ const readonlySignal = _asReadonly();
2194
+ readonlySignal.isEmpty = internalSignal.isEmpty;
2195
+ readonlySignal.size = internalSignal.size;
2196
+ readonlySignal.asArray = internalSignal.asArray;
2197
+ return readonlySignal;
2198
+ };
2199
+ return internalSignal;
2200
+ }
2201
+
2202
+ /**
2203
+ * Creates a writable stack signal, which is a reactive stack data structure.
2204
+ *
2205
+ * @template T The type of elements held in the stack.
2206
+ * @param {T[]} [initialValue=[]] Optional initial items in the stack.
2207
+ * @returns {WritableStackSignal<T>} The writable stack signal instance.
2208
+ *
2209
+ * @example
2210
+ * const stack = stackSignal<number>([1, 2, 3]);
2211
+ * stack.push(4); // [1, 2, 3, 4]
2212
+ * const value = stack.pop(); // 4 [1, 2, 3]
2213
+ * stack.clear(); // []
2214
+ */
2215
+ function stackSignal(initialValue = []) {
2216
+ const internalSignal = signal([
2217
+ ...initialValue,
2218
+ ]);
2219
+ const _set = internalSignal.set;
2220
+ const _update = internalSignal.update;
2221
+ const _asReadonly = internalSignal.asReadonly;
2222
+ // Native methods
2223
+ internalSignal.set = (arr) => _set([...arr]);
2224
+ internalSignal.update = (updateFn) => {
2225
+ _update((current) => [...updateFn(current)]);
2226
+ };
2227
+ // Stack methods
2228
+ internalSignal.push = (...items) => {
2229
+ internalSignal.update((stack) => [...stack, ...items]);
2230
+ };
2231
+ internalSignal.pop = () => {
2232
+ let popped = undefined;
2233
+ internalSignal.update((stack) => {
2234
+ if (stack.length === 0)
2235
+ return stack;
2236
+ const copy = [...stack];
2237
+ popped = copy.pop();
2238
+ return copy;
2239
+ });
2240
+ return popped;
2241
+ };
2242
+ internalSignal.clear = () => {
2243
+ internalSignal.set([]);
2244
+ };
2245
+ internalSignal.isEmpty = computed(() => internalSignal().length === 0);
2246
+ internalSignal.size = computed(() => internalSignal().length);
2247
+ internalSignal.top = computed(() => {
2248
+ const stack = internalSignal();
2249
+ return stack.length ? stack[stack.length - 1] : undefined;
2250
+ });
2251
+ internalSignal.asArray = computed(() => [...internalSignal()]);
2252
+ internalSignal.peek = () => {
2253
+ const stack = internalSignal();
2254
+ return stack.length ? stack[stack.length - 1] : undefined;
2255
+ };
2256
+ internalSignal.asReadonly = () => {
2257
+ const readonlySignal = _asReadonly();
2258
+ readonlySignal.isEmpty = internalSignal.isEmpty;
2259
+ readonlySignal.size = internalSignal.size;
2260
+ readonlySignal.top = internalSignal.top;
2261
+ readonlySignal.asArray = internalSignal.asArray;
2262
+ readonlySignal.peek = internalSignal.peek;
2263
+ return readonlySignal;
2264
+ };
2265
+ return internalSignal;
2266
+ }
2267
+
2268
+ /**
2269
+ * Creates a `WritableSignal` that limits how often its value can be updated, using the throttle time specified.
2270
+ *
2271
+ * @param initialValue - The initial value of the signal.
2272
+ * @param throttleTime - The minimum delay in milliseconds between signal updates.
2273
+ * @returns A `WritableSignal<T>` that throttles value updates based on the specified throttle time.
2274
+ */
2275
+ function throttledSignal(initialValue, throttleTime) {
2276
+ const internalSignal = signal(initialValue);
2277
+ Object.assign(internalSignal, { throttleTime });
2278
+ let lastUpdate = 0;
2279
+ let queuedValue = undefined;
2280
+ let timeoutId = null;
2281
+ const originalSet = internalSignal.set;
2282
+ const setThrottledValue = (value) => {
2283
+ const now = Date.now();
2284
+ const timeSinceLastUpdate = now - lastUpdate;
2285
+ // If the new value is different and enough time has passed, set it immediately
2286
+ if (timeSinceLastUpdate >= throttleTime && internalSignal() !== value) {
2287
+ lastUpdate = now;
2288
+ originalSet(value);
2289
+ queuedValue = undefined;
2290
+ if (timeoutId) {
2291
+ clearTimeout(timeoutId);
2292
+ timeoutId = null;
2293
+ }
2294
+ }
2295
+ else {
2296
+ if (queuedValue === undefined) {
2297
+ queuedValue = value;
2298
+ timeoutId = setTimeout(() => {
2299
+ if (queuedValue !== null) {
2300
+ originalSet(queuedValue);
2301
+ lastUpdate = Date.now();
2302
+ queuedValue = undefined;
2303
+ timeoutId = null;
2304
+ }
2305
+ }, throttleTime - timeSinceLastUpdate);
2306
+ }
2307
+ else {
2308
+ queuedValue = value;
2309
+ }
2310
+ }
2311
+ };
2312
+ internalSignal.set = (value) => {
2313
+ setThrottledValue(value);
2314
+ };
2315
+ internalSignal.update = (fn) => {
2316
+ setThrottledValue(fn(internalSignal()));
2317
+ };
2318
+ return internalSignal;
2319
+ }
2320
+
2321
+ /**
2322
+ * Creates a writable, reactive tuple signal of **fixed length**, with tuple-specific and array-like helpers.
2323
+ *
2324
+ * This function returns a signal wrapping a tuple (fixed-length array), exposing safe, type-aware mutation and update helpers
2325
+ * as well as computed signals for common tuple queries.
2326
+ *
2327
+ * All mutating methods (`set`, `setAt`, `updateAt`, `update`) return a new tuple value,
2328
+ * preserving immutability and optimal signal reactivity.
2329
+ *
2330
+ * Also provides `.asReadonly()` for read-only consumption and computed signals for empty/entries.
2331
+ *
2332
+ * @template T The tuple type (e.g. `[number, string, boolean]`).
2333
+ * @param {T} initialValue The initial tuple value.
2334
+ * @returns {WritableTupleSignal<T>} A writable tuple signal with all mutation, query, and computed helpers.
2335
+ *
2336
+ * @example
2337
+ * const point = tupleSignal<[number, number]>([0, 1]);
2338
+ * point.setAt(1, 99); // [0, 99]
2339
+ * point.updateAt(0, x => x+10);// [10, 99]
2340
+ * point(); // [10, 99]
2341
+ * point.entriesArray(); // [[0, 10], [1, 99]]
2342
+ */
2343
+ function tupleSignal(initialValue) {
2344
+ // Clone initialValue for safety.
2345
+ const internalSignal = signal([
2346
+ ...initialValue,
2347
+ ]);
2348
+ const _set = internalSignal.set;
2349
+ const _update = internalSignal.update;
2350
+ const _asReadonly = internalSignal.asReadonly;
2351
+ internalSignal.set = (tuple) => _set([...tuple]);
2352
+ internalSignal.update = (updateFn) => {
2353
+ _update((current) => [...updateFn(current)]);
2354
+ };
2355
+ // Set by index (returns a new tuple).
2356
+ internalSignal.setAt = (index, value) => {
2357
+ internalSignal.update((current) => {
2358
+ const next = [...current];
2359
+ next[index] = value;
2360
+ return next;
2361
+ });
2362
+ };
2363
+ // Update by index.
2364
+ internalSignal.updateAt = (index, updateFn) => {
2365
+ internalSignal.update((current) => {
2366
+ const next = [...current];
2367
+ next[index] = updateFn(next[index]);
2368
+ return next;
2369
+ });
2370
+ };
2371
+ // Read-only API.
2372
+ internalSignal.asReadonly = () => {
2373
+ const readonlySignal = _asReadonly();
2374
+ readonlySignal.entriesArray = internalSignal.entriesArray;
2375
+ readonlySignal.getAt = internalSignal.getAt;
2376
+ return readonlySignal;
2377
+ };
2378
+ // Computed helpers.
2379
+ internalSignal.entriesArray = computed(() => {
2380
+ const tuple = internalSignal();
2381
+ return tuple.map((value, index) => [index, value]);
2382
+ });
2383
+ internalSignal.getAt = (index) => internalSignal()[index];
2384
+ return internalSignal;
2385
+ }
2386
+
2387
+ class FormControlTracker {
2388
+ constructor(thisObjOrAbstractControl, options = {}) {
2389
+ this.thisObjOrAbstractControl = thisObjOrAbstractControl;
2390
+ this.options = options;
2391
+ this._injector = this.options.injector ?? inject(Injector);
2392
+ this._value$ = new BehaviorSubject(undefined);
2393
+ this.value$ = this._value$.asObservable();
2394
+ this.value = toSignal(this._value$, {
2395
+ injector: this._injector,
2396
+ initialValue: undefined,
2397
+ });
2398
+ this._errors$ = new BehaviorSubject(null);
2399
+ this.errors$ = this._errors$.asObservable();
2400
+ this.errors = toSignal(this._errors$, {
2401
+ injector: this._injector,
2402
+ initialValue: null,
2403
+ });
2404
+ this._touched$ = new BehaviorSubject(false);
2405
+ this.touched$ = this._touched$.asObservable();
2406
+ this.touched = toSignal(this._touched$, {
2407
+ injector: this._injector,
2408
+ initialValue: false,
2409
+ });
2410
+ this.untouched$ = this.touched$.pipe(map((v) => !v));
2411
+ this.untouched = toSignal(this.untouched$, {
2412
+ injector: this._injector,
2413
+ initialValue: true,
2414
+ });
2415
+ this._pristine$ = new BehaviorSubject(true);
2416
+ this.pristine$ = this._pristine$.asObservable();
2417
+ this.pristine = toSignal(this._pristine$, {
2418
+ injector: this._injector,
2419
+ initialValue: true,
2420
+ });
2421
+ this.dirty$ = this.pristine$.pipe(map((v) => !v));
2422
+ this.dirty = toSignal(this.dirty$, {
2423
+ injector: this._injector,
2424
+ initialValue: false,
2425
+ });
2426
+ this._status$ = new BehaviorSubject('VALID');
2427
+ this.status$ = this._status$.asObservable();
2428
+ this.status = toSignal(this._status$, {
2429
+ injector: this._injector,
2430
+ initialValue: 'VALID',
2431
+ });
2432
+ this.valid$ = this.status$.pipe(map((v) => v === 'VALID'));
2433
+ this.valid = toSignal(this.valid$, {
2434
+ injector: this._injector,
2435
+ initialValue: true,
2436
+ });
2437
+ this.invalid$ = this.status$.pipe(map((v) => v === 'INVALID'));
2438
+ this.invalid = toSignal(this.invalid$, {
2439
+ injector: this._injector,
2440
+ initialValue: false,
2441
+ });
2442
+ this.pending$ = this.status$.pipe(map((v) => v === 'PENDING'));
2443
+ this.pending = toSignal(this.pending$, {
2444
+ injector: this._injector,
2445
+ initialValue: false,
2446
+ });
2447
+ this.disabled$ = this.status$.pipe(map((v) => v === 'DISABLED'));
2448
+ this.disabled = toSignal(this.disabled$, {
2449
+ injector: this._injector,
2450
+ initialValue: false,
2451
+ });
2452
+ this.enabled$ = this.status$.pipe(map((v) => v !== 'DISABLED'));
2453
+ this.enabled = toSignal(this.enabled$, {
2454
+ injector: this._injector,
2455
+ initialValue: true,
2456
+ });
2457
+ this.touchedHasErrors = computed(() => !!this.errors() && this.touched());
2458
+ this._validators$ = new BehaviorSubject(null);
2459
+ this.validators$ = this._validators$.asObservable();
2460
+ this.validators = toSignal(this._validators$, {
2461
+ injector: this._injector,
2462
+ initialValue: null,
2463
+ });
2464
+ this._asyncValidators$ = new BehaviorSubject(null);
2465
+ this.asyncValidators$ = this._asyncValidators$.asObservable();
2466
+ this.asyncValidators = toSignal(this._asyncValidators$, {
2467
+ injector: this._injector,
2468
+ initialValue: null,
2469
+ });
2470
+ if (this._isAbstractControl(this.thisObjOrAbstractControl)) {
2471
+ this.init();
2472
+ }
2473
+ }
2474
+ _isAbstractControl(v) {
2475
+ return (isFormControl(v) ||
2476
+ isFormArray(v) ||
2477
+ isFormGroup(v) ||
2478
+ isFormRecord(v) ||
2479
+ (!!v && typeof v === 'object' && 'events' in v && isObservable(v.events)));
2480
+ }
2481
+ init() {
2482
+ if (this._isAbstractControl(this.thisObjOrAbstractControl)) {
2483
+ this.instance = this.thisObjOrAbstractControl;
2484
+ }
2485
+ else {
2486
+ const ngControl = this._injector.get(NgControl, null);
2487
+ if (ngControl) {
2488
+ // if the valueAccessor was not yet set by Angular, we have to do it manually here
2489
+ if (this.options.attachValueAccessor ?? true) {
2490
+ if (!ngControl.valueAccessor ||
2491
+ (this.thisObjOrAbstractControl &&
2492
+ this.thisObjOrAbstractControl instanceof
2493
+ ngControl.valueAccessor.constructor)) {
2494
+ ngControl.valueAccessor = this
2495
+ .thisObjOrAbstractControl;
2496
+ }
2497
+ }
2498
+ // grab the AbstractControl instance
2499
+ const instance = ngControl.control;
2500
+ if (!instance)
2501
+ return;
2502
+ this.instance = instance;
2503
+ }
2504
+ }
2505
+ if (this.instance) {
2506
+ const instance = this.instance;
2507
+ // listen to all data available in the events emitter
2508
+ this._eventsSub = instance.events.subscribe((event) => {
2509
+ if (event instanceof ValueChangeEvent) {
2510
+ this._value$.next(event.value);
2511
+ return;
2512
+ }
2513
+ if (event instanceof TouchedChangeEvent) {
2514
+ this._touched$.next(event.touched);
2515
+ return;
2516
+ }
2517
+ if (event instanceof PristineChangeEvent) {
2518
+ this._pristine$.next(event.pristine);
2519
+ return;
2520
+ }
2521
+ if (event instanceof StatusChangeEvent) {
2522
+ this._status$.next(event.status);
2523
+ this._errors$.next(event.source.errors);
2524
+ return;
2525
+ }
2526
+ });
2527
+ this._value$.next(instance.value);
2528
+ this._touched$.next(instance.touched);
2529
+ this._pristine$.next(instance.pristine);
2530
+ this._status$.next(instance.status);
2531
+ this._errors$.next(instance.errors);
2532
+ // do not read the next lines of code if you are easily frightened
2533
+ // I'm not proud of this part, but it had to be done. God please forgive me
2534
+ // I didn't find any other feasible way to detect when the control changes its validators
2535
+ // so it had to be hacked like this
2536
+ // override the "setValidators" function to capture the validators
2537
+ const oldSetValidators = instance.setValidators.bind(instance);
2538
+ instance.setValidators = (...args) => {
2539
+ oldSetValidators(...args);
2540
+ const validators = instance._rawValidators;
2541
+ this._validators$.next(isFunction(validators) ? [validators] : validators);
2542
+ };
2543
+ // override the "validator" setter to capture the validators
2544
+ wrapSetter(instance, 'validator', () => {
2545
+ const raw = instance._rawValidators;
2546
+ this._validators$.next(isFunction(raw) ? [raw] : raw);
2547
+ });
2548
+ // override the "setAsyncValidators" function to capture the async validators
2549
+ const oldSetAsyncValidators = instance.setAsyncValidators.bind(instance);
2550
+ instance.setAsyncValidators = (...args) => {
2551
+ oldSetAsyncValidators(...args);
2552
+ const validators = instance._rawAsyncValidators;
2553
+ this._asyncValidators$.next(isFunction(validators) ? [validators] : validators);
2554
+ };
2555
+ // override the "asyncValidator" setter to capture the async validators
2556
+ wrapSetter(instance, 'asyncValidator', () => {
2557
+ const raw = instance._rawAsyncValidators;
2558
+ this._asyncValidators$.next(isFunction(raw) ? [raw] : raw);
2559
+ });
2560
+ // assign the validators at init time
2561
+ const validators = instance._rawValidators;
2562
+ this._validators$.next(isFunction(validators) ? [validators] : validators);
2563
+ const asyncValidators = instance._rawAsyncValidators;
2564
+ this._asyncValidators$.next(isFunction(asyncValidators) ? [asyncValidators] : asyncValidators);
2565
+ }
2566
+ }
2567
+ destroy() {
2568
+ this._eventsSub?.unsubscribe();
2569
+ }
2570
+ }
2571
+ /**
2572
+ * Gives reactive access to the properties of the given form control.
2573
+ *
2574
+ * Call `destroy()` to dispose of form control tracker.
2575
+ *
2576
+ * @param formControl the form control to be tracked.
2577
+ * @param options optional configuration for the form control tracker.
2578
+ * @returns an object containing all standard form control getters as signals.
2579
+ * @example
2580
+ * ```
2581
+ * readonly ageControl = new FormControl('John');
2582
+ *
2583
+ * readonly ageControlTracker = trackFormControl(this.ageControl);
2584
+ * ```
2585
+ */
2586
+ function trackFormControl(formControl, options) {
2587
+ return new FormControlTracker(formControl, {
2588
+ ...options,
2589
+ attachValueAccessor: false,
2590
+ });
2591
+ }
2592
+ /**
2593
+ * Gives reactive access to the properties of the Form Control assigned to the component instance.
2594
+ *
2595
+ * Call the `init()` method inside `ngOnInit()` to start listening to the Form Control, call `destroy()` to dispose of the listener.
2596
+ *
2597
+ * @param thisObj the component instance.
2598
+ * @param options optional configuration for the form control tracker.
2599
+ * @returns an object containing all standard form control getters as signals.
2600
+ * @example
2601
+ * ```
2602
+ * export class MyComponent implements ControlValueAccessor, OnInit, OnDestroy {
2603
+ * readonly control = trackFormControl(this);
2604
+ *
2605
+ * ngOnInit(): void {
2606
+ * this.control.init();
2607
+ * }
2608
+ * ngOnDestroy(): void {
2609
+ * this.control.destroy();
2610
+ * }
2611
+ * // control value accessor implementation ...
2612
+ * }
2613
+ * ```
2614
+ */
2615
+ function trackBoundControl(thisObj, options) {
2616
+ return new FormControlTracker(thisObj, options);
2617
+ }
2618
+ // --- helper: wraps an accessor setter on a single instance ---
2619
+ function wrapSetter(obj, propName, after) {
2620
+ // find the accessor descriptor up the prototype chain
2621
+ let proto = obj;
2622
+ let desc;
2623
+ while (proto && !(desc = Object.getOwnPropertyDescriptor(proto, propName))) {
2624
+ proto = Object.getPrototypeOf(proto);
2625
+ }
2626
+ if (!desc || typeof desc.set !== 'function') {
2627
+ throw new Error(`No setter found for ${String(propName)}`);
2628
+ }
2629
+ const originalSet = desc.set;
2630
+ const originalGet = desc.get; // may exist
2631
+ Object.defineProperty(obj, propName, {
2632
+ configurable: true,
2633
+ enumerable: desc.enumerable ?? true,
2634
+ get: originalGet
2635
+ ? function () {
2636
+ return originalGet.call(this);
2637
+ }
2638
+ : undefined,
2639
+ set: function (value) {
2640
+ originalSet.call(this, value);
2641
+ after();
2642
+ },
2643
+ });
2644
+ }
2645
+
2646
+ /**
2647
+ * Coerces a value to an array of trimmed non-empty strings.
2648
+ * Any input that is not an array, `null` or `undefined` will be turned into a string
2649
+ * using `String()` and split into an array with the given separator.
2650
+ * `null`, `undefined`, and `""` will result in an empty array.
2651
+ * This results in the following outcomes:
2652
+ * - `null` -> `[]`
2653
+ * - `[null]` -> `["null"]`
2654
+ * - `["a", "b ", " "]` -> `["a", "b"]`
2655
+ * - `[1, [2, 3]]` -> `["1", "2,3"]`
2656
+ * - `"a,b , c"` -> `["a", "b", "c"]`
2657
+ *
2658
+ * Useful for defining CSS classes or table columns.
2659
+ * @param value the value to coerce into an array of strings
2660
+ * @param separator split-separator if value isn't an array. Defaults to the string `","`.
2661
+ */
2662
+ function coerceArrayProperty(value, separator = ',') {
2663
+ if (Array.isArray(value))
2664
+ return value.map((v) => String(v).trim()).filter((v) => v.length);
2665
+ if (value == null || value === '')
2666
+ return [];
2667
+ return String(value)
2668
+ .split(separator)
2669
+ .map((v) => v.trim())
2670
+ .filter((v) => v.length);
2671
+ }
2672
+
2673
+ function getUTCDate(year, monthIndex, day, hour = 0, minute = 0, second = 0, millisecond = 0) {
2674
+ if (!isDefined(year) && !isDefined(monthIndex) && !isDefined(day)) {
2675
+ const now = new Date();
2676
+ now.setMinutes(now.getMinutes() + now.getTimezoneOffset());
2677
+ return now;
2678
+ }
2679
+ return new Date(Date.UTC(year, monthIndex, day, hour, minute, second, millisecond));
2680
+ }
2681
+ function coerceDateProperty(v, fallback) {
2682
+ if (typeof v == 'string') {
2683
+ if (v.toLowerCase() === 'now') {
2684
+ return new Date();
2685
+ }
2686
+ }
2687
+ v = new Date(v);
2688
+ if (isNaN(v.valueOf())) {
2689
+ return fallback;
2690
+ }
2691
+ return v;
2692
+ }
2693
+ function coerceDateOnlyProperty(v, fallback, asUTC = false) {
2694
+ if (typeof v == 'string') {
2695
+ if (v.toLowerCase() === 'now' || v.toLowerCase() === 'today') {
2696
+ const now = new Date();
2697
+ now.setMinutes(now.getMinutes() + now.getTimezoneOffset());
2698
+ return asUTC
2699
+ ? getUTCDate(now.getFullYear(), now.getMonth(), now.getDate(), now.getHours(), now.getMinutes(), now.getSeconds(), now.getMilliseconds())
2700
+ : now;
2701
+ }
2702
+ }
2703
+ const dateV = new Date(v);
2704
+ if (isNaN(dateV.valueOf())) {
2705
+ return fallback;
2706
+ }
2707
+ if (dateV.toISOString().substring(10) === 'T00:00:00.000Z') {
2708
+ if (asUTC) {
2709
+ return dateV;
2710
+ }
2711
+ return new Date(dateV.getUTCFullYear(), dateV.getUTCMonth(), dateV.getUTCDate(), 0, 0, 0, 0);
2712
+ }
2713
+ if (asUTC) {
2714
+ return getUTCDate(dateV.getFullYear(), dateV.getMonth(), dateV.getDate());
2715
+ }
2716
+ return new Date(dateV.getFullYear(), dateV.getMonth(), dateV.getDate(), 0, 0, 0, 0);
2717
+ }
2718
+
2719
+ function getDomContentRect(el) {
2720
+ if (el instanceof ElementRef)
2721
+ el = el.nativeElement;
2722
+ const elRect = el.getBoundingClientRect();
2723
+ const [topPadd, rightPadd, bottomPadd, leftPadd] = getFourWayValue(el, 'padding');
2724
+ const [topBorder, rightBorder, bottomBorder, leftBorder] = getFourWayValue(el, 'border-width');
2725
+ return new DOMRect(elRect.x + leftPadd + leftBorder, elRect.y + topPadd + topBorder, elRect.width - leftPadd - rightPadd - leftBorder - rightBorder, elRect.height - topPadd - bottomPadd - topBorder - bottomBorder);
2726
+ }
2727
+ function getDomPaddingRect(el) {
2728
+ if (el instanceof ElementRef)
2729
+ el = el.nativeElement;
2730
+ const elRect = el.getBoundingClientRect();
2731
+ const [topBorder, rightBorder, bottomBorder, leftBorder] = getFourWayValue(el, 'border-width');
2732
+ return new DOMRect(elRect.x + leftBorder, elRect.y + topBorder, elRect.width - leftBorder - rightBorder, elRect.height - topBorder - bottomBorder);
2733
+ }
2734
+ function getFourWayValue(el, v) {
2735
+ const elValues = window.getComputedStyle(el).getPropertyValue(v);
2736
+ const values = elValues.split(' ').map((v) => (v ? parseFloat(v) : null));
2737
+ const topValue = values[0] ?? 0;
2738
+ const rightValue = values[1] ?? topValue;
2739
+ const bottomValue = values[2] ?? topValue;
2740
+ const leftValue = values[3] ?? rightValue ?? topValue;
2741
+ return [topValue, rightValue, bottomValue, leftValue];
2742
+ }
2743
+
2744
+ function findBestSuggestions(toMatch, suggestions, mapFn) {
2745
+ //map non-string values to strings, if needed
2746
+ const suggestionStrings = suggestions.map((v) => typeof v == 'string' ? v : mapFn(v));
2747
+ //* alternative method for single-char strings
2748
+ //the similarity algorithm always returns zero in this case
2749
+ if (toMatch.length == 1) {
2750
+ return (suggestionStrings
2751
+ //remember the original indexes
2752
+ .map((v, index) => ({ str: v, index }))
2753
+ //pick only the ones starting with the character to match
2754
+ .filter((v) => v.str.startsWith(toMatch))
2755
+ //map to string length and index
2756
+ .map((v) => ({ rating: v.str.length, index: v.index }))
2757
+ //sort by length, ascending
2758
+ .sort((a, b) => a.rating - b.rating)
2759
+ //map to original values
2760
+ .map((v) => suggestions[v.index]));
2761
+ }
2762
+ //* standard method
2763
+ return (suggestionStrings
2764
+ //map to ratings of similarity between the string and toMatch
2765
+ .map((v, index) => {
2766
+ //always include if starts with the value to match
2767
+ if (new RegExp(`^${toMatch}`, 'i').test(v))
2768
+ return { rating: 1_000 - v.length, index };
2769
+ //calculate conditional similarity
2770
+ const similarity = diceCoefficient(toMatch, v);
2771
+ const foundTerm = v.match(new RegExp(toMatch, 'ig'))?.[0];
2772
+ const foundIndex = foundTerm ? v.indexOf(foundTerm) : -1;
2773
+ const canBeFound = foundIndex != -1;
2774
+ const foundIndexModifier = canBeFound
2775
+ ? (Math.cos((foundIndex / 90) * Math.PI) + 1) ** 2 / 1.5
2776
+ : 1;
2777
+ const rating = similarity * foundIndexModifier;
2778
+ return {
2779
+ rating,
2780
+ index,
2781
+ };
2782
+ })
2783
+ //keep only ratings above or equal to 0.45
2784
+ .filter((v) => v.rating >= 0.45)
2785
+ //sort descending
2786
+ .sort((a, b) => b.rating - a.rating)
2787
+ //map to original values
2788
+ .map((v) => suggestions[v.index]));
2789
+ }
2790
+ function findBestAutocomplate(toMatch, autocompletes) {
2791
+ const targetArray = autocompletes.filter((v) => new RegExp(`^${toMatch}`, 'i').test(v));
2792
+ let bestMatch = targetArray.shift();
2793
+ let bestScore = 0;
2794
+ for (const str of targetArray) {
2795
+ const score = diceCoefficient(toMatch, str);
2796
+ if (score > bestScore) {
2797
+ bestMatch = str;
2798
+ bestScore = score;
2799
+ }
2800
+ }
2801
+ return bestMatch;
2802
+ }
2803
+
2804
+ class EventRelativePos {
2805
+ constructor(target, top, right, bottom, left) {
2806
+ this.target = target;
2807
+ this.top = top;
2808
+ this.right = right;
2809
+ this.bottom = bottom;
2810
+ this.left = left;
2811
+ this.overflowsTop = top < 0;
2812
+ this.overflowsRight = right < 0;
2813
+ this.overflowsBottom = bottom < 0;
2814
+ this.overflowsLeft = left < 0;
2815
+ this.overflows =
2816
+ this.overflowsTop ||
2817
+ this.overflowsRight ||
2818
+ this.overflowsBottom ||
2819
+ this.overflowsLeft;
2820
+ }
2821
+ valueOf() {
2822
+ return {
2823
+ top: this.top,
2824
+ right: this.right,
2825
+ bottom: this.bottom,
2826
+ left: this.left,
2827
+ };
2828
+ }
2829
+ toJSON() {
2830
+ return JSON.stringify(this.valueOf());
2831
+ }
2832
+ toString() {
2833
+ return this.toJSON();
2834
+ }
2835
+ static fromEvent(event, el) {
2836
+ return getEventRelativePos(event, el);
2837
+ }
2838
+ }
2839
+ function _isTouchEvent(v) {
2840
+ return typeof v === 'object' && 'touches' in v;
2841
+ }
2842
+ function getEventRelativePos(event, el) {
2843
+ //convert ElementRef
2844
+ if (el instanceof ElementRef)
2845
+ el = el.nativeElement;
2846
+ //convert TouchEvent
2847
+ if (_isTouchEvent(event)) {
2848
+ const firstTouch = event.touches.item(0);
2849
+ if (!firstTouch)
2850
+ throw new Error('DKT-FT9000: Cannot read event position. The TouchEvent has no Touch instances.');
2851
+ event = firstTouch;
2852
+ }
2853
+ //calculate
2854
+ const elRect = el.getBoundingClientRect();
2855
+ const eventX = event.clientX;
2856
+ const eventY = event.clientY;
2857
+ return new EventRelativePos(el, eventY - elRect.top, //top
2858
+ elRect.right - eventX, //right
2859
+ elRect.bottom - eventY, //bottom
2860
+ eventX - elRect.left);
2861
+ }
2862
+
2863
+ const ASCII_REGEX = /^[\x00-\x7F]*$/;
2864
+ const BASE64_REGEX = /^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/;
2865
+ const HEX_REGEX = /^[0-9a-fA-F]+$/;
2866
+ const OCTAL_REGEX = /^[0-7]+$/;
2867
+ const BINARY_REGEX = /^[0-1]+$/;
2868
+ const IPV4_REGEX = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
2869
+ const IPV6_REGEX = /^(([0-9a-fA-F]{1,4}:){7}([0-9a-fA-F]{1,4}|:))|(([0-9a-fA-F]{1,4}:){1,7}:)|(([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4})|(([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2})|(([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3})|(([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4})|(([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5})|([0-9a-fA-F]{1,4}:)((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1{0,1}[0-9])?[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1{0,1}[0-9])?[0-9])$/;
2870
+ const UUID_REGEX = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
2871
+ const HEX_COLOR_REGEX = /^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
2872
+ const RGB_COLOR_REGEX = /^rgb\(\s*(0|255|25[0-4]|2[0-4][0-9]|1?[0-9]{1,2})\s*,\s*(0|255|25[0-4]|2[0-4][0-9]|1?[0-9]{1,2})\s*,\s*(0|255|25[0-4]|2[0-4][0-9]|1?[0-9]{1,2})\s*\)$/;
2873
+ const HSL_COLOR_REGEX = /^hsl\(\s*(360|3[0-5][0-9]|[12]?[0-9]{1,2})\s*,\s*(100|[1-9]?[0-9])%\s*,\s*(100|[1-9]?[0-9])%\s*\)$/;
2874
+ class ExtValidators {
2875
+ /**
2876
+ * Validator that requires the control's value to contain a certain substring.
2877
+ *
2878
+ * @param seed The substring that the control's value should contain.
2879
+ * @returns A validator function that returns an error map with the `contains` property if the validation check fails, otherwise `null`.
2880
+ *
2881
+ * @example
2882
+ *
2883
+ * ```ts
2884
+ * const control = new FormControl('ng', ExtValidators.contains('angular'));
2885
+ * console.log(control.errors);
2886
+ * // { contains: { actualValue: 'ng', shouldContain: 'angular' } }
2887
+ *
2888
+ * const control = new FormControl('I love angular', ExtValidators.contains('angular'));
2889
+ * console.log(control.errors);
2890
+ * // null
2891
+ * ```
2892
+ */
2893
+ static contains(seed) {
2894
+ return (control) => {
2895
+ const value = control.value;
2896
+ if (typeof value !== 'string') {
2897
+ // don't validate empty values to allow optional controls
2898
+ // don't validate non-string values to avoid false positives
2899
+ return null;
2900
+ }
2901
+ if (value.includes(seed)) {
2902
+ return null;
2903
+ }
2904
+ return { contains: { actualValue: value, shouldContain: seed } };
2905
+ };
2906
+ }
2907
+ /**
2908
+ * Validator that requires the control's value to not contain a certain substring.
2909
+ *
2910
+ * @param seed The substring that the control's value should not contain.
2911
+ * @returns A validator function that returns an error map with the `notContains` property if the validation check fails, otherwise `null`.
2912
+ *
2913
+ * @example
2914
+ *
2915
+ * ```ts
2916
+ * const control = new FormControl('angular', ExtValidators.notContains('ng'));
2917
+ * console.log(control.errors);
2918
+ * // { notContains: { actualValue: 'angular', shouldNotContain: 'ng' } }
2919
+ *
2920
+ * const control = new FormControl('angular', ExtValidators.notContains('react'));
2921
+ * console.log(control.errors);
2922
+ * // null
2923
+ * ```
2924
+ */
2925
+ static notContains(seed) {
2926
+ return (control) => {
2927
+ const value = control.value;
2928
+ if (typeof value !== 'string') {
2929
+ // don't validate empty values to allow optional controls
2930
+ // don't validate non-string values to avoid false positives
2931
+ return null;
2932
+ }
2933
+ if (!value.includes(seed)) {
2934
+ return null;
2935
+ }
2936
+ return { notContains: { actualValue: value, shouldNotContain: seed } };
2937
+ };
2938
+ }
2939
+ /**
2940
+ * Validator that requires the control's value to be in lowercase.
2941
+ *
2942
+ * @returns An error map with the `lowercase` property if the validation check fails, otherwise `null`.
2943
+ *
2944
+ * @example
2945
+ *
2946
+ * ```ts
2947
+ * const control = new FormControl('Angular', ExtValidators.lowercase);
2948
+ * console.log(control.errors);
2949
+ * // { lowercase: { actualValue: 'Angular' } }
2950
+ *
2951
+ * const control = new FormControl('angular', ExtValidators.lowercase);
2952
+ * console.log(control.errors);
2953
+ * // null
2954
+ * ```
2955
+ */
2956
+ static lowercase(control) {
2957
+ const value = control.value;
2958
+ if (typeof value !== 'string') {
2959
+ // don't validate empty values to allow optional controls
2960
+ // don't validate non-string values to avoid false positives
2961
+ return null;
2962
+ }
2963
+ if (value === value.toLowerCase()) {
2964
+ return null;
2965
+ }
2966
+ return { lowercase: { actualValue: value } };
2967
+ }
2968
+ /**
2969
+ * Validator that requires the control's value to be in uppercase.
2970
+ *
2971
+ * @returns An error map with the `uppercase` property if the validation check fails, otherwise `null`.
2972
+ *
2973
+ * @example
2974
+ *
2975
+ * ```ts
2976
+ * const control = new FormControl('Angular', ExtValidators.uppercase);
2977
+ * console.log(control.errors);
2978
+ * // { uppercase: { actualValue: 'Angular' } }
2979
+ *
2980
+ * const control = new FormControl('ANGULAR', ExtValidators.uppercase);
2981
+ * console.log(control.errors);
2982
+ * // null
2983
+ * ```
2984
+ */
2985
+ static uppercase(control) {
2986
+ const value = control.value;
2987
+ if (typeof value !== 'string') {
2988
+ // don't validate empty values to allow optional controls
2989
+ // don't validate non-string values to avoid false positives
2990
+ return null;
2991
+ }
2992
+ if (value === value.toUpperCase()) {
2993
+ return null;
2994
+ }
2995
+ return { uppercase: { actualValue: value } };
2996
+ }
2997
+ /**
2998
+ * Validator that wraps the built-in `Validators.pattern` but allows specifying a custom error name instead of the default `pattern` key.
2999
+ *
3000
+ * @param name The error name to use when the validation fails.
3001
+ * @param pattern A string or RegExp to test the control's value against.
3002
+ * @returns A `ValidatorFn` that returns an error map with the given `name` when the value does not match the pattern.
3003
+ *
3004
+ * @example
3005
+ *
3006
+ * ```ts
3007
+ * const control = new FormControl('abc-123', ExtValidators.namedPattern('sku', /^[A-Z]{3}-\d{3}$/));
3008
+ * console.log(control.errors);
3009
+ * // { sku: { actualValue: 'abc-123' } }
3010
+ *
3011
+ * const control = new FormControl('ABC-123', ExtValidators.namedPattern('sku', /^[A-Z]{3}-\d{3}$/));
3012
+ * console.log(control.errors);
3013
+ * // null
3014
+ * ```
3015
+ */
3016
+ static namedPattern(name, pattern) {
3017
+ const validator = Validators.pattern(pattern);
3018
+ return (control) => {
3019
+ const validationResult = validator(control);
3020
+ if (validationResult === null) {
3021
+ return null;
3022
+ }
3023
+ return {
3024
+ [name]: { actualValue: validationResult['pattern'].actualValue },
3025
+ };
3026
+ };
3027
+ }
3028
+ /**
3029
+ * Validator that requires the control's numeric value to be divisible by the given divisor.
3030
+ *
3031
+ * - Accepts numbers or numeric strings (uses `parseFloat`).
3032
+ * - Does not validate `null`/`undefined` values to allow optional controls.
3033
+ * - If `divisor` is `null`/`undefined`/`0`, validation is skipped and `null` is returned.
3034
+ *
3035
+ * @param divisor The number the control's value must be divisible by.
3036
+ * @returns A validator function that returns `{ divisibleBy: { divisor, actual } }` when invalid, otherwise `null`.
3037
+ *
3038
+ * @example
3039
+ *
3040
+ * ```ts
3041
+ * const control = new FormControl('12', ExtValidators.divisibleBy(5));
3042
+ * console.log(control.errors);
3043
+ * // { divisibleBy: { divisor: 5, actual: '12' } }
3044
+ *
3045
+ * const control = new FormControl('15', ExtValidators.divisibleBy(5));
3046
+ * console.log(control.errors);
3047
+ * // null
3048
+ * ```
3049
+ */
3050
+ static divisibleBy(divisor) {
3051
+ return (control) => {
3052
+ if (control.value == null || divisor == null || divisor === 0) {
3053
+ return null; // don't validate empty values to allow optional controls
3054
+ }
3055
+ const value = parseFloat(control.value);
3056
+ // Controls with NaN values after parsing should be treated as not having a
3057
+ // divisor, per the HTML forms spec: https://www.w3.org/TR/html5/forms.html#attr-input-min
3058
+ return !isNaN(value) && value % divisor !== 0
3059
+ ? { divisibleBy: { divisor: divisor, actual: control.value } }
3060
+ : null;
3061
+ };
3062
+ }
3063
+ /**
3064
+ * Validator that requires the control's value to contain only ASCII characters.
3065
+ *
3066
+ * @param control The form control to validate.
3067
+ * @returns `null` when valid (or when value is not a string), otherwise `{ ascii: { actualValue } }`.
3068
+ *
3069
+ * @example
3070
+ *
3071
+ * ```ts
3072
+ * const control = new FormControl('Zażółć', ExtValidators.ascii);
3073
+ * console.log(control.errors);
3074
+ * // { ascii: { actualValue: 'Zażółć' } }
3075
+ *
3076
+ * const control = new FormControl('Hello!', ExtValidators.ascii);
3077
+ * console.log(control.errors);
3078
+ * // null
3079
+ * ```
3080
+ */
3081
+ static ascii(control) {
3082
+ const value = control.value;
3083
+ if (typeof value !== 'string') {
3084
+ // don't validate empty values to allow optional controls
3085
+ // don't validate non-string values to avoid false positives
3086
+ return null;
3087
+ }
3088
+ return ASCII_REGEX.test(value) ? null : { ascii: { actualValue: value } };
3089
+ }
3090
+ /**
3091
+ * Validator that requires the control's value to be valid Base64 (RFC 4648-style alphabet).
3092
+ *
3093
+ * Notes:
3094
+ * - This checks *format* (characters + padding), not whether the decoded bytes are meaningful.
3095
+ * - Non-string / empty values are not validated to allow optional controls.
3096
+ *
3097
+ * @param control The form control to validate.
3098
+ * @returns `null` when valid (or when value is not a string), otherwise `{ base64: { actualValue } }`.
3099
+ *
3100
+ * @example
3101
+ *
3102
+ * ```ts
3103
+ * const control = new FormControl('hello world', ExtValidators.base64);
3104
+ * console.log(control.errors);
3105
+ * // { base64: { actualValue: 'hello world' } }
3106
+ *
3107
+ * const control = new FormControl('aGVsbG8=', ExtValidators.base64);
3108
+ * console.log(control.errors);
3109
+ * // null
3110
+ * ```
3111
+ */
3112
+ static base64(control) {
3113
+ const value = control.value;
3114
+ if (typeof value !== 'string') {
3115
+ // don't validate empty values to allow optional controls
3116
+ // don't validate non-string values to avoid false positives
3117
+ return null;
3118
+ }
3119
+ return BASE64_REGEX.test(value) ? null : { base64: { actualValue: value } };
3120
+ }
3121
+ /**
3122
+ * Validator that requires the control's value to be a hexadecimal string.
3123
+ *
3124
+ * - Accepts upper and lower case characters.
3125
+ * - Does not allow `0x` prefix (pure hex digits only).
3126
+ *
3127
+ * @param control The form control to validate.
3128
+ * @returns `null` when valid (or when value is not a string), otherwise `{ hex: { actualValue } }`.
3129
+ *
3130
+ * @example
3131
+ *
3132
+ * ```ts
3133
+ * const control = new FormControl('0xdeadbeef', ExtValidators.hex);
3134
+ * console.log(control.errors);
3135
+ * // { hex: { actualValue: '0xdeadbeef' } }
3136
+ *
3137
+ * const control = new FormControl('deadBEEF', ExtValidators.hex);
3138
+ * console.log(control.errors);
3139
+ * // null
3140
+ * ```
3141
+ */
3142
+ static hex(control) {
3143
+ const value = control.value;
3144
+ if (typeof value !== 'string') {
3145
+ // don't validate empty values to allow optional controls
3146
+ // don't validate non-string values to avoid false positives
3147
+ return null;
3148
+ }
3149
+ return HEX_REGEX.test(value) ? null : { hex: { actualValue: value } };
3150
+ }
3151
+ /**
3152
+ * Validator that requires the control's value to be an octal string (digits 0-7 only).
3153
+ *
3154
+ * @param control The form control to validate.
3155
+ * @returns `null` when valid (or when value is not a string), otherwise `{ octal: { actualValue } }`.
3156
+ *
3157
+ * @example
3158
+ *
3159
+ * ```ts
3160
+ * const control = new FormControl('128', ExtValidators.octal);
3161
+ * console.log(control.errors);
3162
+ * // { octal: { actualValue: '128' } }
3163
+ *
3164
+ * const control = new FormControl('755', ExtValidators.octal);
3165
+ * console.log(control.errors);
3166
+ * // null
3167
+ * ```
3168
+ */
3169
+ static octal(control) {
3170
+ const value = control.value;
3171
+ if (typeof value !== 'string') {
3172
+ // don't validate empty values to allow optional controls
3173
+ // don't validate non-string values to avoid false positives
3174
+ return null;
3175
+ }
3176
+ return OCTAL_REGEX.test(value) ? null : { octal: { actualValue: value } };
3177
+ }
3178
+ /**
3179
+ * Validator that requires the control's value to be a binary string (digits 0-1 only).
3180
+ *
3181
+ * @param control The form control to validate.
3182
+ * @returns `null` when valid (or when value is not a string), otherwise `{ binary: { actualValue } }`.
3183
+ *
3184
+ * @example
3185
+ *
3186
+ * ```ts
3187
+ * const control = new FormControl('10201', ExtValidators.binary);
3188
+ * console.log(control.errors);
3189
+ * // { binary: { actualValue: '10201' } }
3190
+ *
3191
+ * const control = new FormControl('101010', ExtValidators.binary);
3192
+ * console.log(control.errors);
3193
+ * // null
3194
+ * ```
3195
+ */
3196
+ static binary(control) {
3197
+ const value = control.value;
3198
+ if (typeof value !== 'string') {
3199
+ // don't validate empty values to allow optional controls
3200
+ // don't validate non-string values to avoid false positives
3201
+ return null;
3202
+ }
3203
+ return BINARY_REGEX.test(value) ? null : { binary: { actualValue: value } };
3204
+ }
3205
+ /**
3206
+ * Validator that requires the control's value to be a valid IP address.
3207
+ *
3208
+ * @param ipType The required IP version (`4` for IPv4, `6` for IPv6).
3209
+ * @returns A validator function that returns `{ ipAddress: { requiredType, actualValue } }` when invalid, otherwise `null`.
3210
+ *
3211
+ * @example
3212
+ *
3213
+ * ```ts
3214
+ * const control = new FormControl('999.1.2.3', ExtValidators.ipAddress(4));
3215
+ * console.log(control.errors);
3216
+ * // { ipAddress: { requiredType: 'IPv4', actualValue: '999.1.2.3' } }
3217
+ *
3218
+ * const control = new FormControl('127.0.0.1', ExtValidators.ipAddress(4));
3219
+ * console.log(control.errors);
3220
+ * // null
3221
+ * ```
3222
+ *
3223
+ * ```ts
3224
+ * const control = new FormControl('not-an-ip', ExtValidators.ipAddress(6));
3225
+ * console.log(control.errors);
3226
+ * // { ipAddress: { requiredType: 'IPv6', actualValue: 'not-an-ip' } }
3227
+ *
3228
+ * const control = new FormControl('::1', ExtValidators.ipAddress(6));
3229
+ * console.log(control.errors);
3230
+ * // null
3231
+ * ```
3232
+ */
3233
+ static ipAddress(ipType) {
3234
+ return (control) => {
3235
+ const value = control.value;
3236
+ if (typeof value !== 'string') {
3237
+ // don't validate empty values to allow optional controls
3238
+ // don't validate non-string values to avoid false positives
3239
+ return null;
3240
+ }
3241
+ if (ipType === 6) {
3242
+ return IPV6_REGEX.test(value)
3243
+ ? null
3244
+ : {
3245
+ ipAddress: {
3246
+ requiredType: 6,
3247
+ actualValue: value,
3248
+ },
3249
+ };
3250
+ }
3251
+ if (ipType === 4) {
3252
+ return IPV4_REGEX.test(value)
3253
+ ? null
3254
+ : { ipAddress: { requiredType: 4, actualValue: value } };
3255
+ }
3256
+ return IPV4_REGEX.test(value) || IPV6_REGEX.test(value)
3257
+ ? null
3258
+ : {
3259
+ ipAddress: { requiredType: 'any', actualValue: value },
3260
+ };
3261
+ };
3262
+ }
3263
+ /**
3264
+ * Validator that requires the control's value to be a UUID string.
3265
+ *
3266
+ * Notes:
3267
+ * - This validates the canonical 8-4-4-4-12 hex format.
3268
+ * - It does not enforce UUID version/variant bits (it accepts any hex in those positions).
3269
+ *
3270
+ * @param control The form control to validate.
3271
+ * @returns `null` when valid (or when value is not a string), otherwise `{ uuid: { actualValue } }`.
3272
+ *
3273
+ * @example
3274
+ *
3275
+ * ```ts
3276
+ * const control = new FormControl('550e8400-e29b-41d4-a716-44665544ZZZZ', ExtValidators.uuid);
3277
+ * console.log(control.errors);
3278
+ * // { uuid: { actualValue: '550e8400-e29b-41d4-a716-44665544ZZZZ' } }
3279
+ *
3280
+ * const control = new FormControl('550e8400-e29b-41d4-a716-446655440000', ExtValidators.uuid);
3281
+ * console.log(control.errors);
3282
+ * // null
3283
+ * ```
3284
+ */
3285
+ static uuid(control) {
3286
+ const value = control.value;
3287
+ if (typeof value !== 'string') {
3288
+ // don't validate empty values to allow optional controls
3289
+ // don't validate non-string values to avoid false positives
3290
+ return null;
3291
+ }
3292
+ return UUID_REGEX.test(value) ? null : { uuid: { actualValue: value } };
3293
+ }
3294
+ /**
3295
+ * Validator that requires the control's value to be a hex color.
3296
+ *
3297
+ * Accepts:
3298
+ * - `#RGB` / `RGB`
3299
+ * - `#RRGGBB` / `RRGGBB`
3300
+ *
3301
+ * @param control The form control to validate.
3302
+ * @returns `null` when valid (or when value is not a string), otherwise `{ hexColor: { actualValue } }`.
3303
+ *
3304
+ * @example
3305
+ *
3306
+ * ```ts
3307
+ * const control = new FormControl('#12FG00', ExtValidators.hexColor);
3308
+ * console.log(control.errors);
3309
+ * // { hexColor: { actualValue: '#12FG00' } }
3310
+ *
3311
+ * const control = new FormControl('#ff00aa', ExtValidators.hexColor);
3312
+ * console.log(control.errors);
3313
+ * // null
3314
+ * ```
3315
+ */
3316
+ static hexColor(control) {
3317
+ const value = control.value;
3318
+ if (typeof value !== 'string') {
3319
+ // don't validate empty values to allow optional controls
3320
+ // don't validate non-string values to avoid false positives
3321
+ return null;
3322
+ }
3323
+ return HEX_COLOR_REGEX.test(value)
3324
+ ? null
3325
+ : { hexColor: { actualValue: value } };
3326
+ }
3327
+ /**
3328
+ * Validator that requires the control's value to be an `rgb(r, g, b)` CSS color string.
3329
+ *
3330
+ * Constraints:
3331
+ * - `r`, `g`, `b` must be integers in the range 0..255
3332
+ * - Whitespace is allowed
3333
+ *
3334
+ * @param control The form control to validate.
3335
+ * @returns `null` when valid (or when value is not a string), otherwise `{ rgbColor: { actualValue } }`.
3336
+ *
3337
+ * @example
3338
+ *
3339
+ * ```ts
3340
+ * const control = new FormControl('rgb(256, 0, 0)', ExtValidators.rgbColor);
3341
+ * console.log(control.errors);
3342
+ * // { rgbColor: { actualValue: 'rgb(256, 0, 0)' } }
3343
+ *
3344
+ * const control = new FormControl('rgb(255, 0, 128)', ExtValidators.rgbColor);
3345
+ * console.log(control.errors);
3346
+ * // null
3347
+ * ```
3348
+ */
3349
+ static rgbColor(control) {
3350
+ const value = control.value;
3351
+ if (typeof value !== 'string') {
3352
+ // don't validate empty values to allow optional controls
3353
+ // don't validate non-string values to avoid false positives
3354
+ return null;
3355
+ }
3356
+ return RGB_COLOR_REGEX.test(value)
3357
+ ? null
3358
+ : { rgbColor: { actualValue: value } };
3359
+ }
3360
+ /**
3361
+ * Validator that requires the control's value to be an `hsl(h, s%, l%)` CSS color string.
3362
+ *
3363
+ * Constraints:
3364
+ * - `h` (hue) must be an integer in the range 0..360
3365
+ * - `s` and `l` must be percentages in the range 0..100
3366
+ * - Whitespace is allowed
3367
+ *
3368
+ * @param control The form control to validate.
3369
+ * @returns `null` when valid (or when value is not a string), otherwise `{ hslColor: { actualValue } }`.
3370
+ *
3371
+ * @example
3372
+ *
3373
+ * ```ts
3374
+ * const control = new FormControl('hsl(361, 50%, 40%)', ExtValidators.hslColor);
3375
+ * console.log(control.errors);
3376
+ * // { hslColor: { actualValue: 'hsl(361, 50%, 40%)' } }
3377
+ *
3378
+ * const control = new FormControl('hsl(210, 50%, 40%)', ExtValidators.hslColor);
3379
+ * console.log(control.errors);
3380
+ * // null
3381
+ * ```
3382
+ */
3383
+ static hslColor(control) {
3384
+ const value = control.value;
3385
+ if (typeof value !== 'string') {
3386
+ // don't validate empty values to allow optional controls
3387
+ // don't validate non-string values to avoid false positives
3388
+ return null;
3389
+ }
3390
+ return HSL_COLOR_REGEX.test(value)
3391
+ ? null
3392
+ : { hslColor: { actualValue: value } };
3393
+ }
3394
+ /**
3395
+ * Validator factory for password-like rules.
3396
+ *
3397
+ * You can combine any subset of rules. Only rules that are enabled will be validated.
3398
+ *
3399
+ * Returned error shape:
3400
+ * - `null` when valid
3401
+ * - `{ password: { ...details } }` when invalid, where `details` may include:
3402
+ * - `minLength` / `maxLength` objects with required/actual lengths
3403
+ * - boolean flags: `requireNumbers`, `requireUppercase`, `requireLowercase`, `requireSpecialCharacters`
3404
+ *
3405
+ * Notes:
3406
+ * - Special character set is: `[!@#$%^&*(),.?":{}|<>]`
3407
+ * - Non-string / empty values are not validated to allow optional controls.
3408
+ *
3409
+ * @param rules Password rules configuration.
3410
+ * @returns A validator function that returns a `password` error map when validation fails, otherwise `null`.
3411
+ *
3412
+ * @example
3413
+ *
3414
+ * Negative example (produces an error and shows the nested details):
3415
+ *
3416
+ * ```ts
3417
+ * const control = new FormControl('password', ExtValidators.password({
3418
+ * minLength: 8,
3419
+ * requireNumbers: true,
3420
+ * requireUppercase: true,
3421
+ * requireSpecialCharacters: true,
3422
+ * }));
3423
+ *
3424
+ * console.log(control.errors);
3425
+ * // {
3426
+ * // password: {
3427
+ * // requireNumbers: true,
3428
+ * // requireUppercase: true,
3429
+ * // requireSpecialCharacters: true
3430
+ * // }
3431
+ * // }
3432
+ *
3433
+ * const control = new FormControl('P@ssw0rd!', ExtValidators.password({
3434
+ * minLength: 8,
3435
+ * requireNumbers: true,
3436
+ * requireUppercase: true,
3437
+ * requireSpecialCharacters: true,
3438
+ * }));
3439
+ *
3440
+ * console.log(control.errors);
3441
+ * // null
3442
+ * ```
3443
+ */
3444
+ static password(rules) {
3445
+ return (control) => {
3446
+ const value = control.value;
3447
+ if (typeof value !== 'string') {
3448
+ // don't validate empty values to allow optional controls
3449
+ // don't validate non-string values to avoid false positives
3450
+ return null;
3451
+ }
3452
+ const errors = {};
3453
+ if (rules.minLength != null && value.length < rules.minLength) {
3454
+ errors.minLength = {
3455
+ requiredLength: rules.minLength,
3456
+ actualLength: value.length,
3457
+ };
3458
+ }
3459
+ if (rules.maxLength != null && value.length > rules.maxLength) {
3460
+ errors.maxLength = {
3461
+ requiredLength: rules.maxLength,
3462
+ actualLength: value.length,
3463
+ };
3464
+ }
3465
+ if (rules.requireNumbers && !/\d/.test(value)) {
3466
+ errors.requireNumbers = true;
3467
+ }
3468
+ if (rules.requireUppercase && !/[A-Z]/.test(value)) {
3469
+ errors.requireUppercase = true;
3470
+ }
3471
+ if (rules.requireLowercase && !/[a-z]/.test(value)) {
3472
+ errors.requireLowercase = true;
3473
+ }
3474
+ if (rules.requireSpecialCharacters &&
3475
+ !/[!@#$%^&*(),.?":{}|<>]/.test(value)) {
3476
+ errors.requireSpecialCharacters = true;
3477
+ }
3478
+ return Object.keys(errors).length > 0 ? { password: errors } : null;
3479
+ };
3480
+ }
3481
+ /**
3482
+ * Validator that requires the control's value (an array) to contain a specific element.
3483
+ *
3484
+ * Non-array values are ignored to avoid false positives.
3485
+ *
3486
+ * @param seed The element that must exist in the array (uses `Array.prototype.includes`).
3487
+ * @returns A validator function that returns `{ arrayContains: { actualValue, shouldContain } }` when invalid, otherwise `null`.
3488
+ *
3489
+ * @example
3490
+ *
3491
+ * ```ts
3492
+ * const control = new FormControl(['a', 'b'], ExtValidators.arrayContains('c'));
3493
+ * console.log(control.errors);
3494
+ * // { arrayContains: { actualValue: ['a', 'b'], shouldContain: 'c' } }
3495
+ *
3496
+ * const control = new FormControl(['a', 'b', 'c'], ExtValidators.arrayContains('c'));
3497
+ * console.log(control.errors);
3498
+ * // null
3499
+ * ```
3500
+ */
3501
+ static arrayContains(seed) {
3502
+ return (control) => {
3503
+ const value = control.value;
3504
+ if (!Array.isArray(value)) {
3505
+ // don't validate non-array values to avoid false positives
3506
+ return null;
3507
+ }
3508
+ if (value.includes(seed)) {
3509
+ return null;
3510
+ }
3511
+ return { arrayContains: { actualValue: value, shouldContain: seed } };
3512
+ };
3513
+ }
3514
+ /**
3515
+ * Validator that requires the control's value (an array) to *not* contain a specific element.
3516
+ *
3517
+ * Non-array values are ignored to avoid false positives.
3518
+ *
3519
+ * @param seed The element that must not exist in the array (uses `Array.prototype.includes`).
3520
+ * @returns A validator function that returns `{ arrayNotContains: { actualValue, shouldNotContain } }` when invalid, otherwise `null`.
3521
+ *
3522
+ * @example
3523
+ *
3524
+ * ```ts
3525
+ * const control = new FormControl(['a', 'b'], ExtValidators.arrayNotContains('b'));
3526
+ * console.log(control.errors);
3527
+ * // { arrayNotContains: { actualValue: ['a', 'b'], shouldNotContain: 'b' } }
3528
+ *
3529
+ * const control = new FormControl(['a', 'b'], ExtValidators.arrayNotContains('c'));
3530
+ * console.log(control.errors);
3531
+ * // null
3532
+ * ```
3533
+ */
3534
+ static arrayNotContains(seed) {
3535
+ return (control) => {
3536
+ const value = control.value;
3537
+ if (!Array.isArray(value)) {
3538
+ // don't validate non-array values to avoid false positives
3539
+ return null;
3540
+ }
3541
+ if (!value.includes(seed)) {
3542
+ return null;
3543
+ }
3544
+ return {
3545
+ arrayNotContains: { actualValue: value, shouldNotContain: seed },
3546
+ };
3547
+ };
3548
+ }
3549
+ /**
3550
+ * Validator that requires the control's value (an array) to contain only unique items.
3551
+ *
3552
+ * Uses a `Set` to compare uniqueness (`Set.size === array.length`).
3553
+ *
3554
+ * @param control The form control to validate.
3555
+ * @returns `null` when valid (or when value is not an array), otherwise `{ arrayUnique: { actualValue } }`.
3556
+ *
3557
+ * @example
3558
+ *
3559
+ * ```ts
3560
+ * const control = new FormControl([1, 1, 2], ExtValidators.arrayUnique);
3561
+ * console.log(control.errors);
3562
+ * // { arrayUnique: { actualValue: [1, 1, 2] } }
3563
+ *
3564
+ * const control = new FormControl([1, 2, 3], ExtValidators.arrayUnique);
3565
+ * console.log(control.errors);
3566
+ * // null
3567
+ * ```
3568
+ */
3569
+ static arrayUnique(control) {
3570
+ const value = control.value;
3571
+ if (!Array.isArray(value)) {
3572
+ // don't validate non-array values to avoid false positives
3573
+ return null;
3574
+ }
3575
+ const uniqueValues = new Set(value);
3576
+ if (uniqueValues.size === value.length) {
3577
+ return null;
3578
+ }
3579
+ return { arrayUnique: { actualValue: value } };
3580
+ }
3581
+ /**
3582
+ * Validator factory that requires uploaded file(s) to be at most the given size.
3583
+ *
3584
+ * Supported control value shapes:
3585
+ * - a single `File`
3586
+ * - an array of files (typically `File[]`)
3587
+ *
3588
+ * Non-file values (or arrays with no `File` instances) are ignored to avoid false positives.
3589
+ *
3590
+ * Error shape:
3591
+ * - If a single `File` was provided: `{ fileMaxSize: { requiredMaxSize, actualSize } }`
3592
+ * - If an array was provided: `{ fileMaxSize: { requiredMaxSize, actualSizes } }` where `actualSizes` are the sizes of invalid files
3593
+ *
3594
+ * @param maxSizeInBytes Maximum allowed size in bytes.
3595
+ * @returns A validator function that returns a `fileMaxSize` error map when invalid, otherwise `null`.
3596
+ *
3597
+ * @example
3598
+ *
3599
+ * Negative example (single file; produces an error):
3600
+ *
3601
+ * ```ts
3602
+ * const big = new File([new Uint8Array(10)], 'big.txt'); // example only
3603
+ * Object.defineProperty(big, 'size', { value: 10_000_000 }); // force size for demo
3604
+ *
3605
+ * const control = new FormControl<File | null>(big, ExtValidators.fileMaxSize(1_000));
3606
+ * console.log(control.errors);
3607
+ * // { fileMaxSize: { requiredMaxSize: 1000, actualSize: 10000000 } }
3608
+ * ```
3609
+ *
3610
+ * Negative example (array; produces an error with `actualSizes`):
3611
+ *
3612
+ * ```ts
3613
+ * const f1 = new File([new Uint8Array(1)], 'a.txt');
3614
+ * const f2 = new File([new Uint8Array(1)], 'b.txt');
3615
+ * Object.defineProperty(f1, 'size', { value: 500 });
3616
+ * Object.defineProperty(f2, 'size', { value: 1500 });
3617
+ *
3618
+ * const control = new FormControl<File[]>([f1, f2], ExtValidators.fileMaxSize(1000));
3619
+ * console.log(control.errors);
3620
+ * // { fileMaxSize: { requiredMaxSize: 1000, actualSizes: [1500] } }
3621
+ *
3622
+ * const f = new File([new Uint8Array(1)], 'ok.txt');
3623
+ * Object.defineProperty(f, 'size', { value: 500 });
3624
+ *
3625
+ * const control = new FormControl<File | null>(f, ExtValidators.fileMaxSize(1000));
3626
+ * console.log(control.errors);
3627
+ * // null
3628
+ * ```
3629
+ */
3630
+ static fileMaxSize(maxSizeInBytes) {
3631
+ return (control) => {
3632
+ const value = control.value;
3633
+ if (value == null) {
3634
+ return null;
3635
+ }
3636
+ let isSingleFile = false;
3637
+ let files = null;
3638
+ if (value instanceof File) {
3639
+ files = [value];
3640
+ isSingleFile = true;
3641
+ }
3642
+ else if (Array.isArray(value)) {
3643
+ files = value;
3644
+ }
3645
+ else {
3646
+ // don't validate non-file/non-array values to avoid false positives
3647
+ return null;
3648
+ }
3649
+ const fileInstances = files.filter((f) => f instanceof File);
3650
+ if (fileInstances.length === 0) {
3651
+ return null;
3652
+ }
3653
+ const invalid = fileInstances.filter((f) => f.size > maxSizeInBytes);
3654
+ if (invalid.length === 0) {
3655
+ return null;
3656
+ }
3657
+ // preserve single-file shape when only one file was provided
3658
+ if (isSingleFile) {
3659
+ return {
3660
+ fileMaxSize: {
3661
+ requiredMaxSize: maxSizeInBytes,
3662
+ actualSize: fileInstances[0].size,
3663
+ },
3664
+ };
3665
+ }
3666
+ return {
3667
+ fileMaxSize: {
3668
+ requiredMaxSize: maxSizeInBytes,
3669
+ actualSizes: invalid.map((f) => f.size),
3670
+ },
3671
+ };
3672
+ };
3673
+ }
3674
+ /**
3675
+ * Validator factory that requires uploaded file(s) to be at least the given size.
3676
+ *
3677
+ * Supported control value shapes:
3678
+ * - a single `File`
3679
+ * - an array of files (typically `File[]`)
3680
+ *
3681
+ * Non-file values (or arrays with no `File` instances) are ignored to avoid false positives.
3682
+ *
3683
+ * Error shape:
3684
+ * - If a single `File` was provided: `{ fileMinSize: { requiredMinSize, actualSize } }`
3685
+ * - If an array was provided: `{ fileMinSize: { requiredMinSize, actualSizes } }` where `actualSizes` are the sizes of invalid files
3686
+ *
3687
+ * @param minSizeInBytes Minimum allowed size in bytes.
3688
+ * @returns A validator function that returns a `fileMinSize` error map when invalid, otherwise `null`.
3689
+ *
3690
+ * @example
3691
+ *
3692
+ * Negative example (single file; produces an error):
3693
+ *
3694
+ * ```ts
3695
+ * const tiny = new File([new Uint8Array(1)], 'tiny.txt');
3696
+ * Object.defineProperty(tiny, 'size', { value: 50 });
3697
+ *
3698
+ * const control = new FormControl<File | null>(tiny, ExtValidators.fileMinSize(100));
3699
+ * console.log(control.errors);
3700
+ * // { fileMinSize: { requiredMinSize: 100, actualSize: 50 } }
3701
+ * ```
3702
+ *
3703
+ * Negative example (array; produces an error with `actualSizes`):
3704
+ *
3705
+ * ```ts
3706
+ * const f1 = new File([new Uint8Array(1)], 'a.txt');
3707
+ * const f2 = new File([new Uint8Array(1)], 'b.txt');
3708
+ * Object.defineProperty(f1, 'size', { value: 50 });
3709
+ * Object.defineProperty(f2, 'size', { value: 200 });
3710
+ *
3711
+ * const control = new FormControl<File[]>([f1, f2], ExtValidators.fileMinSize(100));
3712
+ * console.log(control.errors);
3713
+ * // { fileMinSize: { requiredMinSize: 100, actualSizes: [50] } }
3714
+ *
3715
+ * const ok = new File([new Uint8Array(1)], 'ok.txt');
3716
+ * Object.defineProperty(ok, 'size', { value: 150 });
3717
+ *
3718
+ * const control = new FormControl<File | null>(ok, ExtValidators.fileMinSize(100));
3719
+ * console.log(control.errors);
3720
+ * // null
3721
+ * ```
3722
+ */
3723
+ static fileMinSize(minSizeInBytes) {
3724
+ return (control) => {
3725
+ const value = control.value;
3726
+ if (value == null) {
3727
+ return null;
3728
+ }
3729
+ let isSingleFile = false;
3730
+ let files = null;
3731
+ if (value instanceof File) {
3732
+ files = [value];
3733
+ isSingleFile = true;
3734
+ }
3735
+ else if (Array.isArray(value)) {
3736
+ files = value;
3737
+ }
3738
+ else {
3739
+ // don't validate non-file/non-array values to avoid false positives
3740
+ return null;
3741
+ }
3742
+ const fileInstances = files.filter((f) => f instanceof File);
3743
+ if (fileInstances.length === 0) {
3744
+ return null;
3745
+ }
3746
+ const invalid = fileInstances.filter((f) => f.size < minSizeInBytes);
3747
+ if (invalid.length === 0) {
3748
+ return null;
3749
+ }
3750
+ if (isSingleFile) {
3751
+ return {
3752
+ fileMinSize: {
3753
+ requiredMinSize: minSizeInBytes,
3754
+ actualSize: fileInstances[0].size,
3755
+ },
3756
+ };
3757
+ }
3758
+ return {
3759
+ fileMinSize: {
3760
+ requiredMinSize: minSizeInBytes,
3761
+ actualSizes: invalid.map((f) => f.size),
3762
+ },
3763
+ };
3764
+ };
3765
+ }
3766
+ /**
3767
+ * Validator factory that requires uploaded file(s) to have an allowed extension.
3768
+ *
3769
+ * Supported control value shapes:
3770
+ * - a single `File`
3771
+ * - an array of files (typically `File[]`)
3772
+ *
3773
+ * Notes:
3774
+ * - Extension is derived from the last `.` in the filename: `file.name.split('.').pop()`
3775
+ * - The extracted extension is lowercased before checking.
3776
+ * - `allowedExtensions` should be provided in lowercase to match the comparison.
3777
+ *
3778
+ * Error shape:
3779
+ * - If a single `File` was provided: `{ fileExtension: { allowedExtensions, actualExtension } }`
3780
+ * - If an array was provided: `{ fileExtension: { allowedExtensions, invalidFiles } }`
3781
+ *
3782
+ * @param allowedExtensions A list of allowed extensions (without dots), e.g. `['png', 'jpg', 'pdf']`.
3783
+ * @returns A validator function that returns a `fileExtension` error map when invalid, otherwise `null`.
3784
+ *
3785
+ * @example
3786
+ *
3787
+ * Negative example (single file; produces an error):
3788
+ *
3789
+ * ```ts
3790
+ * const file = new File([new Uint8Array(1)], 'report.exe');
3791
+ * const control = new FormControl<File | null>(file, ExtValidators.fileExtension(['pdf', 'docx']));
3792
+ * console.log(control.errors);
3793
+ * // { fileExtension: { allowedExtensions: ['pdf', 'docx'], actualExtension: 'exe' } }
3794
+ * ```
3795
+ *
3796
+ * Negative example (array; produces an error with invalid file names):
3797
+ *
3798
+ * ```ts
3799
+ * const a = new File([new Uint8Array(1)], 'a.png');
3800
+ * const b = new File([new Uint8Array(1)], 'b.exe');
3801
+ *
3802
+ * const control = new FormControl<File[]>([a, b], ExtValidators.fileExtension(['png', 'jpg']));
3803
+ * console.log(control.errors);
3804
+ * // { fileExtension: { allowedExtensions: ['png', 'jpg'], invalidFiles: ['b.exe'] } }
3805
+ *
3806
+ * const file = new File([new Uint8Array(1)], 'photo.jpg');
3807
+ * const control = new FormControl<File | null>(file, ExtValidators.fileExtension(['png', 'jpg']));
3808
+ * console.log(control.errors);
3809
+ * // null
3810
+ * ```
3811
+ */
3812
+ static fileExtension(allowedExtensions) {
3813
+ return (control) => {
3814
+ const value = control.value;
3815
+ if (value == null) {
3816
+ return null;
3817
+ }
3818
+ let isSingleFile = false;
3819
+ let files = null;
3820
+ if (value instanceof File) {
3821
+ files = [value];
3822
+ isSingleFile = true;
3823
+ }
3824
+ else if (Array.isArray(value)) {
3825
+ files = value;
3826
+ }
3827
+ else {
3828
+ // don't validate non-file/non-array values to avoid false positives
3829
+ return null;
3830
+ }
3831
+ const fileInstances = files.filter((f) => f instanceof File);
3832
+ if (fileInstances.length === 0) {
3833
+ return null;
3834
+ }
3835
+ const invalidFiles = fileInstances.filter((f) => {
3836
+ const ext = f.name.split('.').pop()?.toLowerCase() || '';
3837
+ return !allowedExtensions.includes(ext);
3838
+ });
3839
+ if (invalidFiles.length === 0) {
3840
+ return null;
3841
+ }
3842
+ if (isSingleFile) {
3843
+ const fileExtension = fileInstances[0].name
3844
+ .split('.')
3845
+ .pop()
3846
+ ?.toLowerCase();
3847
+ return {
3848
+ fileExtension: {
3849
+ allowedExtensions: allowedExtensions,
3850
+ actualExtension: fileExtension,
3851
+ },
3852
+ };
3853
+ }
3854
+ return {
3855
+ fileExtension: {
3856
+ allowedExtensions: allowedExtensions,
3857
+ invalidFiles: invalidFiles.map((f) => f.name),
3858
+ },
3859
+ };
3860
+ };
3861
+ }
3862
+ }
3863
+
3864
+ /*
3865
+ * Public API Surface of devkit
3866
+ */
3867
+ //! feature group 0 - services
3868
+
3869
+ /**
3870
+ * Generated bundle index. Do not edit.
3871
+ */
3872
+
3873
+ export { ARD_FILE_PIPES_DEFAULTS, ARD_HOLD_DEFAULTS, ARD_INFINITE_SCROLL_DEFAULTS, ARD_VIEWPORT_OBSERVER_DEFAULTS, ArdInfScrollTarget, ArdViewportObserverRef, ArdiumClickOutsideDirective, ArdiumClickOutsideModule, ArdiumEscapeHTMLModule, ArdiumEscapeHTMLPipe, ArdiumFileExtensionPipe, ArdiumFileNamePipe, ArdiumFilePipesModule, ArdiumFileSizePipe, ArdiumHoldDirective, ArdiumHoldModule, ArdiumInfiniteScrollDirective, ArdiumInfiniteScrollModule, ArdiumViewportObserverService, EventRelativePos, ExtValidators, FileSystemMethod, FileSystemService, FileSystemStartDirectory, KeyboardService, PersistentStorageMethod, arraySignal, coerceArrayProperty, coerceBooleanProperty, coerceDateOnlyProperty, coerceDateProperty, coerceNumberProperty, counterSignal, createHttpService, debouncedSignal, escapeHTML, findBestAutocomplate, findBestSuggestions, formatFileExtension, formatFileName, formatFileSize, getDomContentRect, getDomPaddingRect, getEventRelativePos, getUTCDate, mapSignal, persistentSignal, provideFilePipesDefaults, provideHoldDefaults, provideInfiniteScrollDefaults, provideViewportObserverDefaults, queryParamSignal, queueSignal, setSignal, stackSignal, throttledSignal, trackBoundControl, trackFormControl, tupleSignal };
3874
+ //# sourceMappingURL=ardium-pl-devkit.mjs.map