@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.
- package/README.md +25 -0
- package/fesm2022/ardium-pl-devkit.mjs +3874 -0
- package/fesm2022/ardium-pl-devkit.mjs.map +1 -0
- package/index.d.ts +5 -0
- package/lib/click-outside/click-outside.directive.d.ts +14 -0
- package/lib/click-outside/click-outside.module.d.ts +8 -0
- package/lib/click-outside/index.d.ts +2 -0
- package/lib/coercion/array.d.ts +21 -0
- package/lib/coercion/boolean.d.ts +6 -0
- package/lib/coercion/date.d.ts +5 -0
- package/lib/coercion/index.d.ts +4 -0
- package/lib/coercion/number.d.ts +20 -0
- package/lib/dom-boxes/dom-boxes.d.ts +7 -0
- package/lib/dom-boxes/index.d.ts +1 -0
- package/lib/escape-html/escape-html.d.ts +1 -0
- package/lib/escape-html/escape-html.module.d.ts +8 -0
- package/lib/escape-html/escape-html.pipe.d.ts +7 -0
- package/lib/escape-html/index.d.ts +3 -0
- package/lib/file-pipes/file-pipes.defaults.d.ts +8 -0
- package/lib/file-pipes/file-pipes.module.d.ts +10 -0
- package/lib/file-pipes/fileext.pipe.d.ts +39 -0
- package/lib/file-pipes/filename.pipe.d.ts +38 -0
- package/lib/file-pipes/filesize.pipe.d.ts +50 -0
- package/lib/file-pipes/index.d.ts +5 -0
- package/lib/file-system/file-system.service.d.ts +69 -0
- package/lib/file-system/file-system.types.d.ts +186 -0
- package/lib/file-system/index.d.ts +2 -0
- package/lib/find-functions/find-suggestion.d.ts +3 -0
- package/lib/find-functions/index.d.ts +1 -0
- package/lib/forms/index.d.ts +1 -0
- package/lib/forms/track-form-control/index.d.ts +1 -0
- package/lib/forms/track-form-control/track-form-control.d.ts +83 -0
- package/lib/hold/hold.defaults.d.ts +9 -0
- package/lib/hold/hold.directive.d.ts +24 -0
- package/lib/hold/hold.module.d.ts +8 -0
- package/lib/hold/index.d.ts +3 -0
- package/lib/http-service/_types.d.ts +20 -0
- package/lib/http-service/_utils.d.ts +1 -0
- package/lib/http-service/http.service.d.ts +2574 -0
- package/lib/http-service/index.d.ts +1 -0
- package/lib/infinite-scroll/index.d.ts +3 -0
- package/lib/infinite-scroll/infinite-scroll.defaults.d.ts +8 -0
- package/lib/infinite-scroll/infinite-scroll.directive.d.ts +24 -0
- package/lib/infinite-scroll/infinite-scroll.module.d.ts +8 -0
- package/lib/keyboard/index.d.ts +2 -0
- package/lib/keyboard/keyboard.service.d.ts +60 -0
- package/lib/keyboard/keyboard.types.d.ts +13 -0
- package/lib/keyboard/types-internal.d.ts +20 -0
- package/lib/keyboard/utils.d.ts +3 -0
- package/lib/relative-pos/index.d.ts +1 -0
- package/lib/relative-pos/relative-pos.d.ts +24 -0
- package/lib/signals/array/array-signal.d.ts +123 -0
- package/lib/signals/array/index.d.ts +1 -0
- package/lib/signals/counter/counter-signal.d.ts +67 -0
- package/lib/signals/counter/index.d.ts +1 -0
- package/lib/signals/debounced/debounced-signal.d.ts +15 -0
- package/lib/signals/debounced/index.d.ts +1 -0
- package/lib/signals/map/index.d.ts +1 -0
- package/lib/signals/map/map-signal.d.ts +97 -0
- package/lib/signals/persistent/index.d.ts +1 -0
- package/lib/signals/persistent/persistent-signal.d.ts +106 -0
- package/lib/signals/query-param/index.d.ts +1 -0
- package/lib/signals/query-param/query-param-signal.d.ts +35 -0
- package/lib/signals/queue/index.d.ts +1 -0
- package/lib/signals/queue/queue-signal.d.ts +77 -0
- package/lib/signals/set/index.d.ts +1 -0
- package/lib/signals/set/set-signal.d.ts +76 -0
- package/lib/signals/stack/index.d.ts +1 -0
- package/lib/signals/stack/stack-signal.d.ts +77 -0
- package/lib/signals/throttled/index.d.ts +1 -0
- package/lib/signals/throttled/throttled-signal.d.ts +15 -0
- package/lib/signals/tuple/index.d.ts +1 -0
- package/lib/signals/tuple/tuple-signal.d.ts +60 -0
- package/lib/validators/index.d.ts +1 -0
- package/lib/validators/validators.d.ts +612 -0
- package/lib/viewport-observer/index.d.ts +3 -0
- package/lib/viewport-observer/utils.d.ts +15 -0
- package/lib/viewport-observer/viewport-observer-ref.d.ts +47 -0
- package/lib/viewport-observer/viewport-observer.defaults.d.ts +7 -0
- package/lib/viewport-observer/viewport-observer.service.d.ts +23 -0
- package/package.json +30 -0
- 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
|
+
'&': '&',
|
|
941
|
+
'<': '<',
|
|
942
|
+
'>': '>',
|
|
943
|
+
'"': '"',
|
|
944
|
+
"'": ''',
|
|
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
|