@etsoo/shared 1.2.52 → 1.2.54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/.github/workflows/main.yml +6 -5
  2. package/lib/cjs/ActionResult.d.ts +1 -1
  3. package/lib/cjs/ActionResult.js +3 -3
  4. package/lib/cjs/ArrayUtils.d.ts +1 -1
  5. package/lib/cjs/ArrayUtils.js +4 -4
  6. package/lib/cjs/ColorUtils.d.ts +1 -1
  7. package/lib/cjs/ColorUtils.js +2 -2
  8. package/lib/cjs/DataTypes.d.ts +6 -6
  9. package/lib/cjs/DataTypes.js +50 -51
  10. package/lib/cjs/DateUtils.d.ts +1 -1
  11. package/lib/cjs/DateUtils.js +27 -28
  12. package/lib/cjs/DomUtils.d.ts +3 -3
  13. package/lib/cjs/DomUtils.js +64 -73
  14. package/lib/cjs/ExtendUtils.d.ts +1 -1
  15. package/lib/cjs/ExtendUtils.js +6 -6
  16. package/lib/cjs/IActionResult.d.ts +1 -1
  17. package/lib/cjs/NumberUtils.d.ts +1 -1
  18. package/lib/cjs/NumberUtils.js +9 -9
  19. package/lib/cjs/StorageUtils.js +2 -2
  20. package/lib/cjs/Utils.d.ts +4 -4
  21. package/lib/cjs/Utils.js +58 -62
  22. package/lib/cjs/index.d.ts +22 -22
  23. package/lib/cjs/storage/WindowStorage.d.ts +1 -1
  24. package/lib/cjs/types/ContentDisposition.d.ts +2 -2
  25. package/lib/cjs/types/ContentDisposition.js +11 -13
  26. package/lib/cjs/types/EColor.js +5 -7
  27. package/lib/cjs/types/EHistory.d.ts +3 -3
  28. package/lib/cjs/types/EHistory.js +4 -4
  29. package/lib/cjs/types/ErrorData.d.ts +1 -1
  30. package/lib/cjs/types/EventClass.js +1 -1
  31. package/lib/mjs/ActionResult.d.ts +1 -1
  32. package/lib/mjs/ActionResult.js +3 -3
  33. package/lib/mjs/ArrayUtils.d.ts +1 -1
  34. package/lib/mjs/ArrayUtils.js +5 -5
  35. package/lib/mjs/ColorUtils.d.ts +1 -1
  36. package/lib/mjs/ColorUtils.js +3 -3
  37. package/lib/mjs/DataTypes.d.ts +6 -6
  38. package/lib/mjs/DataTypes.js +50 -51
  39. package/lib/mjs/DateUtils.d.ts +1 -1
  40. package/lib/mjs/DateUtils.js +27 -28
  41. package/lib/mjs/DomUtils.d.ts +3 -3
  42. package/lib/mjs/DomUtils.js +67 -76
  43. package/lib/mjs/ExtendUtils.d.ts +1 -1
  44. package/lib/mjs/ExtendUtils.js +6 -6
  45. package/lib/mjs/IActionResult.d.ts +1 -1
  46. package/lib/mjs/NumberUtils.d.ts +1 -1
  47. package/lib/mjs/NumberUtils.js +9 -9
  48. package/lib/mjs/StorageUtils.js +4 -4
  49. package/lib/mjs/Utils.d.ts +4 -4
  50. package/lib/mjs/Utils.js +61 -65
  51. package/lib/mjs/index.d.ts +22 -22
  52. package/lib/mjs/index.js +22 -22
  53. package/lib/mjs/storage/WindowStorage.d.ts +1 -1
  54. package/lib/mjs/storage/WindowStorage.js +2 -2
  55. package/lib/mjs/types/ContentDisposition.d.ts +2 -2
  56. package/lib/mjs/types/ContentDisposition.js +12 -14
  57. package/lib/mjs/types/EColor.js +5 -7
  58. package/lib/mjs/types/EHistory.d.ts +3 -3
  59. package/lib/mjs/types/EHistory.js +5 -5
  60. package/lib/mjs/types/ErrorData.d.ts +1 -1
  61. package/lib/mjs/types/EventClass.js +1 -1
  62. package/package.json +61 -63
  63. package/src/ActionResult.ts +23 -23
  64. package/src/ArrayUtils.ts +164 -172
  65. package/src/ColorUtils.ts +80 -82
  66. package/src/DataTypes.ts +745 -754
  67. package/src/DateUtils.ts +266 -268
  68. package/src/DomUtils.ts +806 -831
  69. package/src/ExtendUtils.ts +191 -191
  70. package/src/IActionResult.ts +42 -42
  71. package/src/Keyboard.ts +258 -258
  72. package/src/NumberUtils.ts +135 -135
  73. package/src/StorageUtils.ts +117 -117
  74. package/src/Utils.ts +908 -930
  75. package/src/index.ts +22 -22
  76. package/src/node/Storage.ts +53 -53
  77. package/src/storage/IStorage.ts +62 -62
  78. package/src/storage/WindowStorage.ts +140 -140
  79. package/src/types/ContentDisposition.ts +59 -63
  80. package/src/types/DataError.ts +15 -15
  81. package/src/types/DelayedExecutorType.ts +15 -15
  82. package/src/types/EColor.ts +241 -248
  83. package/src/types/EHistory.ts +151 -151
  84. package/src/types/ErrorData.ts +11 -11
  85. package/src/types/EventClass.ts +220 -220
  86. package/src/types/FormData.ts +25 -25
  87. package/src/types/ParsedPath.ts +5 -5
  88. package/tsconfig.cjs.json +16 -16
  89. package/tsconfig.json +16 -16
  90. package/.eslintignore +0 -3
  91. package/.eslintrc.json +0 -29
  92. package/.prettierignore +0 -5
  93. package/.prettierrc +0 -6
package/src/DomUtils.ts CHANGED
@@ -1,14 +1,14 @@
1
1
  /// <reference lib="dom" />
2
- import { DataTypes } from './DataTypes';
3
- import { DateUtils } from './DateUtils';
4
- import { Utils } from './Utils';
5
- import { ErrorData, ErrorType } from './types/ErrorData';
6
- import { FormDataFieldValue, IFormData } from './types/FormData';
7
-
8
- if (typeof navigator === 'undefined') {
9
- // Test mock only
10
- globalThis.navigator = { language: 'en-US' } as any;
11
- globalThis.location = { href: 'http://localhost/' } as any;
2
+ import { DataTypes } from "./DataTypes";
3
+ import { DateUtils } from "./DateUtils";
4
+ import { Utils } from "./Utils";
5
+ import { ErrorData, ErrorType } from "./types/ErrorData";
6
+ import { FormDataFieldValue, IFormData } from "./types/FormData";
7
+
8
+ if (typeof navigator === "undefined") {
9
+ // Test mock only
10
+ globalThis.navigator = { language: "en-US" } as any;
11
+ globalThis.location = { href: "http://localhost/" } as any;
12
12
  }
13
13
 
14
14
  /**
@@ -16,33 +16,33 @@ if (typeof navigator === 'undefined') {
16
16
  * @see https://developer.mozilla.org/en-US/docs/Web/API/Navigator/userAgentData
17
17
  */
18
18
  export type UserAgentData = {
19
- /**
20
- * Browser brands
21
- */
22
- brands: {
23
- brand: string;
24
- version: string;
25
- }[];
26
-
27
- /**
28
- * Is mobile device
29
- */
30
- mobile: boolean;
31
-
32
- /**
33
- * Device brand (name)
34
- */
35
- device: string;
36
-
37
- /**
38
- * Platform (OS)
39
- */
40
- platform: string;
41
-
42
- /**
43
- * Platform version
44
- */
45
- platformVersion?: string;
19
+ /**
20
+ * Browser brands
21
+ */
22
+ brands: {
23
+ brand: string;
24
+ version: string;
25
+ }[];
26
+
27
+ /**
28
+ * Is mobile device
29
+ */
30
+ mobile: boolean;
31
+
32
+ /**
33
+ * Device brand (name)
34
+ */
35
+ device: string;
36
+
37
+ /**
38
+ * Platform (OS)
39
+ */
40
+ platform: string;
41
+
42
+ /**
43
+ * Platform version
44
+ */
45
+ platformVersion?: string;
46
46
  };
47
47
 
48
48
  /**
@@ -50,867 +50,842 @@ export type UserAgentData = {
50
50
  * Not all methods support Node
51
51
  */
52
52
  export namespace DomUtils {
53
- /**
54
- * Language cache parameter name
55
- */
56
- export const CultureField = 'culture';
57
-
58
- /**
59
- * Country cache parameter name
60
- */
61
- export const CountryField = 'country';
62
-
63
- /**
64
- * Clear form data
65
- * @param data Form data
66
- * @param source Source data to match
67
- * @param keepFields Fields need to be kept
68
- */
69
- export function clearFormData(
70
- data: IFormData,
71
- source?: object,
72
- keepFields?: string[]
73
- ) {
74
- // Unique keys, FormData may have same name keys
75
- const keys = new Set(data.keys());
76
-
77
- // Remove empty key
78
- const removeEmpty = (key: string) => {
79
- // Need to be kept
80
- if (keepFields != null && keepFields.includes(key)) return;
81
-
82
- // Get all values
83
- const formValues = data.getAll(key);
84
- if (formValues.length == 1 && formValues[0] === '') {
85
- // Remove empty field
86
- data.delete(key);
87
- }
88
- };
53
+ /**
54
+ * Language cache parameter name
55
+ */
56
+ export const CultureField = "culture";
57
+
58
+ /**
59
+ * Country cache parameter name
60
+ */
61
+ export const CountryField = "country";
62
+
63
+ /**
64
+ * Clear form data
65
+ * @param data Form data
66
+ * @param source Source data to match
67
+ * @param keepFields Fields need to be kept
68
+ */
69
+ export function clearFormData(
70
+ data: IFormData,
71
+ source?: object,
72
+ keepFields?: string[]
73
+ ) {
74
+ // Unique keys, FormData may have same name keys
75
+ const keys = new Set(data.keys());
76
+
77
+ // Remove empty key
78
+ const removeEmpty = (key: string) => {
79
+ // Need to be kept
80
+ if (keepFields != null && keepFields.includes(key)) return;
81
+
82
+ // Get all values
83
+ const formValues = data.getAll(key);
84
+ if (formValues.length == 1 && formValues[0] === "") {
85
+ // Remove empty field
86
+ data.delete(key);
87
+ }
88
+ };
89
89
 
90
- if (source == null) {
91
- // Remove all empty strings
92
- for (const key of keys) {
93
- removeEmpty(key);
90
+ if (source == null) {
91
+ // Remove all empty strings
92
+ for (const key of keys) {
93
+ removeEmpty(key);
94
+ }
95
+ } else {
96
+ const sourceKeys = Object.keys(source);
97
+ for (const key of sourceKeys) {
98
+ // Need to be kept
99
+ if (keepFields != null && keepFields.includes(key)) continue;
100
+
101
+ // Get all values
102
+ const formValues = data.getAll(key);
103
+ if (formValues.length > 0) {
104
+ // Matched
105
+ // Source value
106
+ const sourceValue = Reflect.get(source, key);
107
+
108
+ if (Array.isArray(sourceValue)) {
109
+ // Array, types may differ
110
+ if (formValues.join("`") === sourceValue.join("`")) {
111
+ // Equal value, remove the key
112
+ data.delete(key);
94
113
  }
95
- } else {
96
- const sourceKeys = Object.keys(source);
97
- for (const key of sourceKeys) {
98
- // Need to be kept
99
- if (keepFields != null && keepFields.includes(key)) continue;
100
-
101
- // Get all values
102
- const formValues = data.getAll(key);
103
- if (formValues.length > 0) {
104
- // Matched
105
- // Source value
106
- const sourceValue = Reflect.get(source, key);
107
-
108
- if (Array.isArray(sourceValue)) {
109
- // Array, types may differ
110
- if (formValues.join('`') === sourceValue.join('`')) {
111
- // Equal value, remove the key
112
- data.delete(key);
113
- }
114
- } else if (formValues.length == 1) {
115
- // Other
116
- if (formValues[0].toString() === `${sourceValue}`) {
117
- // Equal value, remove the key
118
- data.delete(key);
119
- }
120
- }
121
- }
122
- }
123
-
124
- // Left fields
125
- for (const key of keys) {
126
- // Already cleared
127
- if (sourceKeys.includes(key)) continue;
128
-
129
- // Remove empties
130
- removeEmpty(key);
114
+ } else if (formValues.length == 1) {
115
+ // Other
116
+ if (formValues[0].toString() === `${sourceValue}`) {
117
+ // Equal value, remove the key
118
+ data.delete(key);
131
119
  }
120
+ }
132
121
  }
122
+ }
133
123
 
134
- // Return
135
- return data;
136
- }
137
-
138
- function dataAsTraveller(
139
- source: IFormData | object,
140
- data: object,
141
- template: object,
142
- keepSource: boolean,
143
- isValue: boolean
144
- ) {
145
- // Properties
146
- const properties = Object.keys(template);
147
-
148
- // Entries
149
- const entries = Object.entries(
150
- isFormData(source) ? formDataToObject(source) : source
151
- );
124
+ // Left fields
125
+ for (const key of keys) {
126
+ // Already cleared
127
+ if (sourceKeys.includes(key)) continue;
152
128
 
153
- for (const [key, value] of entries) {
154
- // Is included or keepSource
155
- const property =
156
- properties.find(
157
- (p) =>
158
- p.localeCompare(key, 'en', { sensitivity: 'base' }) ===
159
- 0
160
- ) ?? (keepSource ? key : undefined);
161
- if (property == null) continue;
162
-
163
- // Template value
164
- const templateValue = Reflect.get(template, property);
165
-
166
- // Formatted value
167
- let propertyValue: any;
168
-
169
- if (templateValue == null) {
170
- // Just read the source value
171
- propertyValue = value;
172
- } else {
173
- if (isValue) {
174
- // With template value
175
- propertyValue = DataTypes.convert(value, templateValue);
176
- } else {
177
- // With template type
178
- propertyValue = DataTypes.convertByType(
179
- value,
180
- templateValue
181
- );
182
- }
183
- }
184
-
185
- // Set value
186
- // Object.assign(data, { [property]: propertyValue });
187
- // Object.defineProperty(data, property, { value: propertyValue });
188
- Reflect.set(data, property, propertyValue);
189
- }
129
+ // Remove empties
130
+ removeEmpty(key);
131
+ }
190
132
  }
191
133
 
192
- /**
193
- * Cast data as template format
194
- * @param source Source data
195
- * @param template Format template
196
- * @param keepSource Keep other source properties
197
- * @returns Result
198
- */
199
- export function dataAs<T extends DataTypes.BasicTemplate>(
200
- source: unknown,
201
- template: T,
202
- keepSource: boolean = false
203
- ): DataTypes.BasicTemplateType<T> {
204
- // New data
205
- // Object.create(...)
206
- const data = <DataTypes.BasicTemplateType<T>>{};
207
-
208
- if (source != null && typeof source === 'object') {
209
- // Travel all properties
210
- dataAsTraveller(source, data, template, keepSource, false);
134
+ // Return
135
+ return data;
136
+ }
137
+
138
+ function dataAsTraveller(
139
+ source: IFormData | object,
140
+ data: object,
141
+ template: object,
142
+ keepSource: boolean,
143
+ isValue: boolean
144
+ ) {
145
+ // Properties
146
+ const properties = Object.keys(template);
147
+
148
+ // Entries
149
+ const entries = Object.entries(
150
+ isFormData(source) ? formDataToObject(source) : source
151
+ );
152
+
153
+ for (const [key, value] of entries) {
154
+ // Is included or keepSource
155
+ const property =
156
+ properties.find(
157
+ (p) => p.localeCompare(key, "en", { sensitivity: "base" }) === 0
158
+ ) ?? (keepSource ? key : undefined);
159
+ if (property == null) continue;
160
+
161
+ // Template value
162
+ const templateValue = Reflect.get(template, property);
163
+
164
+ // Formatted value
165
+ let propertyValue: any;
166
+
167
+ if (templateValue == null) {
168
+ // Just read the source value
169
+ propertyValue = value;
170
+ } else {
171
+ if (isValue) {
172
+ // With template value
173
+ propertyValue = DataTypes.convert(value, templateValue);
174
+ } else {
175
+ // With template type
176
+ propertyValue = DataTypes.convertByType(value, templateValue);
211
177
  }
178
+ }
212
179
 
213
- // Return
214
- return data;
180
+ // Set value
181
+ // Object.assign(data, { [property]: propertyValue });
182
+ // Object.defineProperty(data, property, { value: propertyValue });
183
+ Reflect.set(data, property, propertyValue);
215
184
  }
216
-
217
- /**
218
- * Cast data to target type
219
- * @param source Source data
220
- * @param template Template for generation
221
- * @param keepSource Means even the template does not include the definition, still keep the item
222
- * @returns Result
223
- */
224
- export function dataValueAs<T extends object>(
225
- source: IFormData | object,
226
- templateValue: T,
227
- keepSource: boolean = false
228
- ): Partial<T> {
229
- // New data
230
- // Object.create(...)
231
- const data = <Partial<T>>{};
232
-
233
- // Travel all properties
234
- dataAsTraveller(source, data, templateValue, keepSource, true);
235
-
236
- // Return
237
- return data;
185
+ }
186
+
187
+ /**
188
+ * Cast data as template format
189
+ * @param source Source data
190
+ * @param template Format template
191
+ * @param keepSource Keep other source properties
192
+ * @returns Result
193
+ */
194
+ export function dataAs<T extends DataTypes.BasicTemplate>(
195
+ source: unknown,
196
+ template: T,
197
+ keepSource: boolean = false
198
+ ): DataTypes.BasicTemplateType<T> {
199
+ // New data
200
+ // Object.create(...)
201
+ const data = <DataTypes.BasicTemplateType<T>>{};
202
+
203
+ if (source != null && typeof source === "object") {
204
+ // Travel all properties
205
+ dataAsTraveller(source, data, template, keepSource, false);
238
206
  }
239
207
 
240
- /**
241
- * Current detected country
242
- */
243
- export const detectedCountry = (() => {
244
- // URL first, then local storage
245
- let country: string | null;
246
- try {
247
- country =
248
- new URL(location.href).searchParams.get(CountryField) ??
249
- sessionStorage.getItem(CountryField) ??
250
- localStorage.getItem(CountryField);
251
- } catch {
252
- country = null;
253
- }
254
-
255
- // Return
256
- return country;
257
- })();
258
-
259
- /**
260
- * Current detected culture
261
- */
262
- export const detectedCulture = (() => {
263
- // URL first, then local storage
264
- let culture: string | null;
265
- try {
266
- culture =
267
- new URL(location.href).searchParams.get(CultureField) ??
268
- sessionStorage.getItem(CultureField) ??
269
- localStorage.getItem(CultureField);
270
- } catch {
271
- culture = null;
272
- }
273
-
274
- // Browser detected
275
- if (culture == null) {
276
- culture =
277
- (navigator.languages && navigator.languages[0]) ||
278
- navigator.language;
279
- }
280
-
281
- // Return
282
- return culture;
283
- })();
284
-
285
- /**
286
- * Is two dimensions equal
287
- * @param d1 Dimension 1
288
- * @param d2 Dimension 2
289
- */
290
- export function dimensionEqual(d1?: DOMRect, d2?: DOMRect) {
291
- if (d1 == null && d2 == null) {
292
- return true;
293
- }
294
-
295
- if (d1 == null || d2 == null) {
296
- return false;
297
- }
298
-
299
- if (
300
- d1.left === d2.left &&
301
- d1.top === d2.top &&
302
- d1.right === d2.right &&
303
- d1.bottom === d2.bottom
304
- ) {
305
- return true;
306
- }
307
-
308
- return false;
208
+ // Return
209
+ return data;
210
+ }
211
+
212
+ /**
213
+ * Cast data to target type
214
+ * @param source Source data
215
+ * @param template Template for generation
216
+ * @param keepSource Means even the template does not include the definition, still keep the item
217
+ * @returns Result
218
+ */
219
+ export function dataValueAs<T extends object>(
220
+ source: IFormData | object,
221
+ templateValue: T,
222
+ keepSource: boolean = false
223
+ ): Partial<T> {
224
+ // New data
225
+ // Object.create(...)
226
+ const data = <Partial<T>>{};
227
+
228
+ // Travel all properties
229
+ dataAsTraveller(source, data, templateValue, keepSource, true);
230
+
231
+ // Return
232
+ return data;
233
+ }
234
+
235
+ /**
236
+ * Current detected country
237
+ */
238
+ export const detectedCountry = (() => {
239
+ // URL first, then local storage
240
+ let country: string | null;
241
+ try {
242
+ country =
243
+ new URL(location.href).searchParams.get(CountryField) ??
244
+ sessionStorage.getItem(CountryField) ??
245
+ localStorage.getItem(CountryField);
246
+ } catch {
247
+ country = null;
309
248
  }
310
249
 
311
- /**
312
- * Download file from API fetch response body
313
- * @param data Data
314
- * @param suggestedName Suggested file name
315
- * @param autoDetect Auto detect, false will use link click way
316
- */
317
- export async function downloadFile(
318
- data: ReadableStream | Blob,
319
- suggestedName?: string,
320
- autoDetect: boolean = true
321
- ) {
322
- try {
323
- if (autoDetect && 'showSaveFilePicker' in globalThis) {
324
- // AbortError - Use dismisses the window
325
- const handle = await (globalThis as any).showSaveFilePicker({
326
- suggestedName
327
- });
328
-
329
- if (!(await verifyPermission(handle, true))) return undefined;
330
-
331
- const stream = await handle.createWritable();
332
-
333
- if (data instanceof Blob) {
334
- data.stream().pipeTo(stream);
335
- } else {
336
- await data.pipeTo(stream);
337
- }
338
-
339
- return true;
340
- } else {
341
- const url = window.URL.createObjectURL(
342
- data instanceof Blob
343
- ? data
344
- : await new Response(data).blob()
345
- );
346
-
347
- const a = document.createElement('a');
348
- a.style.display = 'none';
349
- a.href = url;
350
- if (suggestedName) a.download = suggestedName;
351
-
352
- document.body.appendChild(a);
353
- a.click();
354
- a.remove();
355
-
356
- window.URL.revokeObjectURL(url);
357
-
358
- return true;
359
- }
360
- } catch (e) {
361
- console.error('DomUtils.downloadFile with error', e);
362
- }
363
-
364
- return false;
250
+ // Return
251
+ return country;
252
+ })();
253
+
254
+ /**
255
+ * Current detected culture
256
+ */
257
+ export const detectedCulture = (() => {
258
+ // URL first, then local storage
259
+ let culture: string | null;
260
+ try {
261
+ culture =
262
+ new URL(location.href).searchParams.get(CultureField) ??
263
+ sessionStorage.getItem(CultureField) ??
264
+ localStorage.getItem(CultureField);
265
+ } catch {
266
+ culture = null;
365
267
  }
366
268
 
367
- /**
368
- * File to data URL
369
- * @param file File
370
- * @returns Data URL
371
- */
372
- export async function fileToDataURL(file: File) {
373
- return new Promise<string>((resolve, reject) => {
374
- const reader = new FileReader();
375
- reader.onerror = reject;
376
- reader.onload = () => {
377
- const data = reader.result;
378
- if (data == null) {
379
- reject();
380
- return;
381
- }
382
-
383
- resolve(data as string);
384
- };
385
- reader.readAsDataURL(file);
386
- });
269
+ // Browser detected
270
+ if (culture == null) {
271
+ culture =
272
+ (navigator.languages && navigator.languages[0]) || navigator.language;
387
273
  }
388
274
 
389
- /**
390
- * Form data to object
391
- * @param form Form data
392
- * @returns Object
393
- */
394
- export function formDataToObject(form: IFormData) {
395
- const dic: Record<string, FormDataFieldValue | FormDataFieldValue[]> =
396
- {};
397
- for (const key of new Set(form.keys())) {
398
- const values = form.getAll(key);
399
- dic[key] = values.length == 1 ? values[0] : values;
400
- }
401
- return dic;
275
+ // Return
276
+ return culture;
277
+ })();
278
+
279
+ /**
280
+ * Is two dimensions equal
281
+ * @param d1 Dimension 1
282
+ * @param d2 Dimension 2
283
+ */
284
+ export function dimensionEqual(d1?: DOMRect, d2?: DOMRect) {
285
+ if (d1 == null && d2 == null) {
286
+ return true;
402
287
  }
403
288
 
404
- /**
405
- * Is wechat client
406
- * @param data User agent data
407
- * @returns Result
408
- */
409
- export function isWechatClient(data?: UserAgentData | null) {
410
- data ??= parseUserAgent();
411
- if (!data) return false;
412
-
413
- return data.brands.some(
414
- (item) => item.brand.toLowerCase() === 'micromessenger'
415
- );
289
+ if (d1 == null || d2 == null) {
290
+ return false;
416
291
  }
417
292
 
418
- /**
419
- * Culture match case Enum
420
- */
421
- export enum CultureMatch {
422
- Exact,
423
- Compatible,
424
- SamePart,
425
- Default
293
+ if (
294
+ d1.left === d2.left &&
295
+ d1.top === d2.top &&
296
+ d1.right === d2.right &&
297
+ d1.bottom === d2.bottom
298
+ ) {
299
+ return true;
426
300
  }
427
301
 
428
- /**
429
- * Get English resources definition
430
- * @param resources Resources
431
- * @returns Result
432
- */
433
- export const en = <
434
- T extends DataTypes.StringRecord = DataTypes.StringRecord
435
- >(
436
- resources: T | (() => Promise<T>)
437
- ): DataTypes.CultureDefinition<T> => ({
438
- name: 'en',
439
- label: 'English',
440
- resources
441
- });
302
+ return false;
303
+ }
304
+
305
+ /**
306
+ * Download file from API fetch response body
307
+ * @param data Data
308
+ * @param suggestedName Suggested file name
309
+ * @param autoDetect Auto detect, false will use link click way
310
+ */
311
+ export async function downloadFile(
312
+ data: ReadableStream | Blob,
313
+ suggestedName?: string,
314
+ autoDetect: boolean = true
315
+ ) {
316
+ try {
317
+ if (autoDetect && "showSaveFilePicker" in globalThis) {
318
+ // AbortError - Use dismisses the window
319
+ const handle = await (globalThis as any).showSaveFilePicker({
320
+ suggestedName
321
+ });
442
322
 
443
- /**
444
- * Get simplified Chinese resources definition
445
- * @param resources Resources
446
- * @returns Result
447
- */
448
- export const zhHans = <
449
- T extends DataTypes.StringRecord = DataTypes.StringRecord
450
- >(
451
- resources: T | (() => Promise<T>)
452
- ): DataTypes.CultureDefinition<T> => ({
453
- name: 'zh-Hans',
454
- label: '简体中文',
455
- resources,
456
- compatibleNames: ['zh-CN', 'zh-SG']
457
- });
323
+ if (!(await verifyPermission(handle, true))) return undefined;
458
324
 
459
- /**
460
- * Get traditional Chinese resources definition
461
- * @param resources Resources
462
- * @returns Result
463
- */
464
- export const zhHant = <
465
- T extends DataTypes.StringRecord = DataTypes.StringRecord
466
- >(
467
- resources: T | (() => Promise<T>)
468
- ): DataTypes.CultureDefinition<T> => ({
469
- name: 'zh-Hant',
470
- label: '繁體中文',
471
- resources,
472
- compatibleNames: ['zh-HK', 'zh-TW', 'zh-MO']
473
- });
325
+ const stream = await handle.createWritable();
474
326
 
475
- /**
476
- * Get the available culture definition
477
- * @param items Available cultures
478
- * @param culture Detected culture
479
- */
480
- export const getCulture = <T extends DataTypes.StringRecord>(
481
- items: DataTypes.CultureDefinition<T>[],
482
- culture: string
483
- ): [DataTypes.CultureDefinition<T> | undefined, CultureMatch] => {
484
- if (items.length === 0) {
485
- return [undefined, CultureMatch.Exact];
327
+ if (data instanceof Blob) {
328
+ data.stream().pipeTo(stream);
329
+ } else {
330
+ await data.pipeTo(stream);
486
331
  }
487
332
 
488
- // Exact match
489
- const exactMatch = items.find((item) => item.name === culture);
490
- if (exactMatch) return [exactMatch, CultureMatch.Exact];
491
-
492
- // Compatible match
493
- const compatibleMatch = items.find(
494
- (item) =>
495
- item.compatibleNames?.includes(culture) ||
496
- culture.startsWith(item + '-')
333
+ return true;
334
+ } else {
335
+ const url = window.URL.createObjectURL(
336
+ data instanceof Blob ? data : await new Response(data).blob()
497
337
  );
498
- if (compatibleMatch) return [compatibleMatch, CultureMatch.Compatible];
499
338
 
500
- // Same part, like zh-CN and zh-HK
501
- const samePart = culture.split('-')[0];
502
- const samePartMatch = items.find((item) =>
503
- item.name.startsWith(samePart)
504
- );
505
- if (samePartMatch) return [samePartMatch, CultureMatch.SamePart];
506
-
507
- // Default
508
- return [items[0], CultureMatch.Default];
509
- };
339
+ const a = document.createElement("a");
340
+ a.style.display = "none";
341
+ a.href = url;
342
+ if (suggestedName) a.download = suggestedName;
510
343
 
511
- /**
512
- * Get input value depending on its type
513
- * @param input HTML input
514
- * @returns Result
515
- */
516
- export function getInputValue(input: HTMLInputElement) {
517
- const type = input.type;
518
- if (type === 'number' || type === 'range') {
519
- const num = input.valueAsNumber;
520
- if (isNaN(num)) return null;
521
- return num;
522
- } else if (type === 'date' || type === 'datetime-local')
523
- return input.valueAsDate ?? DateUtils.parse(input.value);
524
- return input.value;
525
- }
344
+ document.body.appendChild(a);
345
+ a.click();
346
+ a.remove();
526
347
 
527
- /**
528
- * Get an unique key combined with current URL
529
- * @param key Key
530
- */
531
- export const getLocationKey = (key: string) => `${location.href}:${key}`;
348
+ window.URL.revokeObjectURL(url);
532
349
 
533
- function isIterable<T>(
534
- headers: Record<string, string> | Iterable<T>
535
- ): headers is Iterable<T> {
536
- return Symbol.iterator in headers;
350
+ return true;
351
+ }
352
+ } catch (e) {
353
+ console.error("DomUtils.downloadFile with error", e);
537
354
  }
538
355
 
539
- /**
540
- * Convert headers to object
541
- * @param headers Heaers
542
- */
543
- export function headersToObject(
544
- headers: HeadersInit | Iterable<[string, string]>
545
- ): Record<string, string> {
546
- if (Array.isArray(headers)) {
547
- return Object.fromEntries(headers);
548
- }
549
-
550
- if (typeof Headers === 'undefined') {
551
- return Object.fromEntries(Object.entries(headers));
356
+ return false;
357
+ }
358
+
359
+ /**
360
+ * File to data URL
361
+ * @param file File
362
+ * @returns Data URL
363
+ */
364
+ export async function fileToDataURL(file: File) {
365
+ return new Promise<string>((resolve, reject) => {
366
+ const reader = new FileReader();
367
+ reader.onerror = reject;
368
+ reader.onload = () => {
369
+ const data = reader.result;
370
+ if (data == null) {
371
+ reject();
372
+ return;
552
373
  }
553
374
 
554
- if (headers instanceof Headers) {
555
- return Object.fromEntries(headers.entries());
556
- }
557
-
558
- if (isIterable(headers)) {
559
- return Object.fromEntries(headers);
560
- }
561
-
562
- return headers;
375
+ resolve(data as string);
376
+ };
377
+ reader.readAsDataURL(file);
378
+ });
379
+ }
380
+
381
+ /**
382
+ * Form data to object
383
+ * @param form Form data
384
+ * @returns Object
385
+ */
386
+ export function formDataToObject(form: IFormData) {
387
+ const dic: Record<string, FormDataFieldValue | FormDataFieldValue[]> = {};
388
+ for (const key of new Set(form.keys())) {
389
+ const values = form.getAll(key);
390
+ dic[key] = values.length == 1 ? values[0] : values;
391
+ }
392
+ return dic;
393
+ }
394
+
395
+ /**
396
+ * Is wechat client
397
+ * @param data User agent data
398
+ * @returns Result
399
+ */
400
+ export function isWechatClient(data?: UserAgentData | null) {
401
+ data ??= parseUserAgent();
402
+ if (!data) return false;
403
+
404
+ return data.brands.some(
405
+ (item) => item.brand.toLowerCase() === "micromessenger"
406
+ );
407
+ }
408
+
409
+ /**
410
+ * Culture match case Enum
411
+ */
412
+ export enum CultureMatch {
413
+ Exact,
414
+ Compatible,
415
+ SamePart,
416
+ Default
417
+ }
418
+
419
+ /**
420
+ * Get English resources definition
421
+ * @param resources Resources
422
+ * @returns Result
423
+ */
424
+ export const en = <T extends DataTypes.StringRecord = DataTypes.StringRecord>(
425
+ resources: T | (() => Promise<T>)
426
+ ): DataTypes.CultureDefinition<T> => ({
427
+ name: "en",
428
+ label: "English",
429
+ resources
430
+ });
431
+
432
+ /**
433
+ * Get simplified Chinese resources definition
434
+ * @param resources Resources
435
+ * @returns Result
436
+ */
437
+ export const zhHans = <
438
+ T extends DataTypes.StringRecord = DataTypes.StringRecord
439
+ >(
440
+ resources: T | (() => Promise<T>)
441
+ ): DataTypes.CultureDefinition<T> => ({
442
+ name: "zh-Hans",
443
+ label: "简体中文",
444
+ resources,
445
+ compatibleNames: ["zh-CN", "zh-SG"]
446
+ });
447
+
448
+ /**
449
+ * Get traditional Chinese resources definition
450
+ * @param resources Resources
451
+ * @returns Result
452
+ */
453
+ export const zhHant = <
454
+ T extends DataTypes.StringRecord = DataTypes.StringRecord
455
+ >(
456
+ resources: T | (() => Promise<T>)
457
+ ): DataTypes.CultureDefinition<T> => ({
458
+ name: "zh-Hant",
459
+ label: "繁體中文",
460
+ resources,
461
+ compatibleNames: ["zh-HK", "zh-TW", "zh-MO"]
462
+ });
463
+
464
+ /**
465
+ * Get the available culture definition
466
+ * @param items Available cultures
467
+ * @param culture Detected culture
468
+ */
469
+ export const getCulture = <T extends DataTypes.StringRecord>(
470
+ items: DataTypes.CultureDefinition<T>[],
471
+ culture: string
472
+ ): [DataTypes.CultureDefinition<T> | undefined, CultureMatch] => {
473
+ if (items.length === 0) {
474
+ return [undefined, CultureMatch.Exact];
563
475
  }
564
476
 
565
- /**
566
- * Is IFormData type guard
567
- * @param input Input object
568
- * @returns result
569
- */
570
- export function isFormData(input: unknown): input is IFormData {
571
- if (
572
- typeof input === 'object' &&
573
- input != null &&
574
- 'entries' in input &&
575
- 'getAll' in input &&
576
- 'keys' in input
577
- ) {
578
- return true;
579
- }
580
- return false;
477
+ // Exact match
478
+ const exactMatch = items.find((item) => item.name === culture);
479
+ if (exactMatch) return [exactMatch, CultureMatch.Exact];
480
+
481
+ // Compatible match
482
+ const compatibleMatch = items.find(
483
+ (item) =>
484
+ item.compatibleNames?.includes(culture) ||
485
+ culture.startsWith(item + "-")
486
+ );
487
+ if (compatibleMatch) return [compatibleMatch, CultureMatch.Compatible];
488
+
489
+ // Same part, like zh-CN and zh-HK
490
+ const samePart = culture.split("-")[0];
491
+ const samePartMatch = items.find((item) => item.name.startsWith(samePart));
492
+ if (samePartMatch) return [samePartMatch, CultureMatch.SamePart];
493
+
494
+ // Default
495
+ return [items[0], CultureMatch.Default];
496
+ };
497
+
498
+ /**
499
+ * Get input value depending on its type
500
+ * @param input HTML input
501
+ * @returns Result
502
+ */
503
+ export function getInputValue(input: HTMLInputElement) {
504
+ const type = input.type;
505
+ if (type === "number" || type === "range") {
506
+ const num = input.valueAsNumber;
507
+ if (isNaN(num)) return null;
508
+ return num;
509
+ } else if (type === "date" || type === "datetime-local")
510
+ return input.valueAsDate ?? DateUtils.parse(input.value);
511
+ return input.value;
512
+ }
513
+
514
+ /**
515
+ * Get an unique key combined with current URL
516
+ * @param key Key
517
+ */
518
+ export const getLocationKey = (key: string) => `${location.href}:${key}`;
519
+
520
+ function isIterable<T>(
521
+ headers: Record<string, string> | Iterable<T>
522
+ ): headers is Iterable<T> {
523
+ return Symbol.iterator in headers;
524
+ }
525
+
526
+ /**
527
+ * Convert headers to object
528
+ * @param headers Heaers
529
+ */
530
+ export function headersToObject(
531
+ headers: HeadersInit | Iterable<[string, string]>
532
+ ): Record<string, string> {
533
+ if (Array.isArray(headers)) {
534
+ return Object.fromEntries(headers);
581
535
  }
582
536
 
583
- /**
584
- * Is JSON content type
585
- * @param contentType Content type string
586
- */
587
- export function isJSONContentType(contentType: string) {
588
- if (
589
- contentType &&
590
- // application/problem+json
591
- // application/json
592
- (contentType.includes('json') ||
593
- contentType.startsWith('application/javascript'))
594
- )
595
- return true;
596
- return false;
537
+ if (typeof Headers === "undefined") {
538
+ return Object.fromEntries(Object.entries(headers));
597
539
  }
598
540
 
599
- /**
600
- * Merge form data to primary one
601
- * @param form Primary form data
602
- * @param forms Other form data
603
- * @returns Merged form data
604
- */
605
- export function mergeFormData(form: IFormData, ...forms: IFormData[]) {
606
- for (const newForm of forms) {
607
- for (const key of new Set(newForm.keys())) {
608
- form.delete(key);
609
- newForm
610
- .getAll(key)
611
- .forEach((value) => form.append(key, value as any));
612
- }
613
- }
541
+ if (headers instanceof Headers) {
542
+ return Object.fromEntries(headers.entries());
543
+ }
614
544
 
615
- return form;
545
+ if (isIterable(headers)) {
546
+ return Object.fromEntries(headers);
616
547
  }
617
548
 
618
- /**
619
- * Merge URL search parameters
620
- * @param base URL search parameters
621
- * @param data New simple object data to merge
622
- */
623
- export function mergeURLSearchParams(
624
- base: URLSearchParams,
625
- data: DataTypes.SimpleObject
549
+ return headers;
550
+ }
551
+
552
+ /**
553
+ * Is IFormData type guard
554
+ * @param input Input object
555
+ * @returns result
556
+ */
557
+ export function isFormData(input: unknown): input is IFormData {
558
+ if (
559
+ typeof input === "object" &&
560
+ input != null &&
561
+ "entries" in input &&
562
+ "getAll" in input &&
563
+ "keys" in input
626
564
  ) {
627
- Object.entries(data).forEach(([key, value]) => {
628
- if (value == null) return;
629
- base.set(key, value.toString());
630
- });
631
- return base;
565
+ return true;
566
+ }
567
+ return false;
568
+ }
569
+
570
+ /**
571
+ * Is JSON content type
572
+ * @param contentType Content type string
573
+ */
574
+ export function isJSONContentType(contentType: string) {
575
+ if (
576
+ contentType &&
577
+ // application/problem+json
578
+ // application/json
579
+ (contentType.includes("json") ||
580
+ contentType.startsWith("application/javascript"))
581
+ )
582
+ return true;
583
+ return false;
584
+ }
585
+
586
+ /**
587
+ * Merge form data to primary one
588
+ * @param form Primary form data
589
+ * @param forms Other form data
590
+ * @returns Merged form data
591
+ */
592
+ export function mergeFormData(form: IFormData, ...forms: IFormData[]) {
593
+ for (const newForm of forms) {
594
+ for (const key of new Set(newForm.keys())) {
595
+ form.delete(key);
596
+ newForm.getAll(key).forEach((value) => form.append(key, value as any));
597
+ }
632
598
  }
633
599
 
634
- /**
635
- * Parse navigator's user agent string
636
- * Lightweight User-Agent string parser
637
- * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent
638
- * @param ua User agent string
639
- * @returns User agent data
640
- */
641
- export function parseUserAgent(ua?: string): UserAgentData | null {
642
- ua ??= globalThis.navigator.userAgent;
643
-
644
- if (!ua) {
645
- return null;
646
- }
647
-
648
- const parts = ua.split(/(?!\(.*)\s+(?!\()(?![^(]*?\))/g);
649
-
650
- let mobile = false;
651
- let platform = '';
652
- let platformVersion: string | undefined;
653
- let device = 'Desktop';
654
- const brands: UserAgentData['brands'] = [];
655
-
656
- // with the 'g' will causing failures for multiple calls
657
- const platformVersionReg =
658
- /^[a-zA-Z0-9-\s]+\s+(0|\d+)(\.(0|\d+)){0,3}(\(|$)/;
659
- const versionReg = /^[a-zA-Z0-9]+\/(0|\d+)(\.(0|\d+)){0,3}(\(|$)/;
660
-
661
- parts.forEach((part) => {
662
- const pl = part.toLowerCase();
663
-
664
- if (pl.startsWith('mozilla/')) {
665
- const data = /\((.*)\)$/.exec(part);
666
- if (data && data.length > 1) {
667
- const pfItems = data[1].split(/;\s*/);
668
-
669
- // Platform + Version
670
- const pfIndex = pfItems.findIndex((item) =>
671
- platformVersionReg.test(item)
672
- );
673
-
674
- if (pfIndex !== -1) {
675
- const pfParts = pfItems[pfIndex].split(/\s+/);
676
- platformVersion = pfParts.pop();
677
- platform = pfParts.join(' ');
678
- } else {
679
- const appleVersionReg =
680
- /((iPhone|Mac)\s+OS(\s+\w+)?)\s+((0|\d+)(_(0|\d+)){0,3})/i;
681
-
682
- for (let i = 0; i < pfItems.length; i++) {
683
- const match = appleVersionReg.exec(pfItems[i]);
684
- if (match && match.length > 4) {
685
- platform = match[1];
686
- platformVersion = match[4].replace(/_/g, '.');
687
-
688
- pfItems.splice(i, 1);
689
- break;
690
- }
691
- }
692
- }
693
-
694
- // Device
695
- const deviceIndex = pfItems.findIndex((item) =>
696
- item.includes(' Build/')
697
- );
698
- if (deviceIndex === -1) {
699
- const firstItem = pfItems[0];
700
- if (
701
- firstItem.toLowerCase() !== 'linux' &&
702
- !firstItem.startsWith(platform)
703
- ) {
704
- device = firstItem;
705
- pfItems.shift();
706
- }
707
- } else {
708
- device = pfItems[deviceIndex].split(' Build/')[0];
709
- pfItems.splice(deviceIndex, 1);
710
- }
711
- }
712
- return;
713
- }
714
-
715
- if (pl === 'mobile' || pl.startsWith('mobile/')) {
716
- mobile = true;
717
- return;
718
- }
600
+ return form;
601
+ }
602
+
603
+ /**
604
+ * Merge URL search parameters
605
+ * @param base URL search parameters
606
+ * @param data New simple object data to merge
607
+ */
608
+ export function mergeURLSearchParams(
609
+ base: URLSearchParams,
610
+ data: DataTypes.SimpleObject
611
+ ) {
612
+ Object.entries(data).forEach(([key, value]) => {
613
+ if (value == null) return;
614
+ base.set(key, value.toString());
615
+ });
616
+ return base;
617
+ }
618
+
619
+ /**
620
+ * Parse navigator's user agent string
621
+ * Lightweight User-Agent string parser
622
+ * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent
623
+ * @param ua User agent string
624
+ * @returns User agent data
625
+ */
626
+ export function parseUserAgent(ua?: string): UserAgentData | null {
627
+ ua ??= globalThis.navigator.userAgent;
628
+
629
+ if (!ua) {
630
+ return null;
631
+ }
719
632
 
720
- if (pl === 'version' || pl.startsWith('version/')) {
721
- // No process
722
- return;
633
+ const parts = ua.split(/(?!\(.*)\s+(?!\()(?![^(]*?\))/g);
634
+
635
+ let mobile = false;
636
+ let platform = "";
637
+ let platformVersion: string | undefined;
638
+ let device = "Desktop";
639
+ const brands: UserAgentData["brands"] = [];
640
+
641
+ // with the 'g' will causing failures for multiple calls
642
+ const platformVersionReg =
643
+ /^[a-zA-Z0-9-\s]+\s+(0|\d+)(\.(0|\d+)){0,3}(\(|$)/;
644
+ const versionReg = /^[a-zA-Z0-9]+\/(0|\d+)(\.(0|\d+)){0,3}(\(|$)/;
645
+
646
+ parts.forEach((part) => {
647
+ const pl = part.toLowerCase();
648
+
649
+ if (pl.startsWith("mozilla/")) {
650
+ const data = /\((.*)\)$/.exec(part);
651
+ if (data && data.length > 1) {
652
+ const pfItems = data[1].split(/;\s*/);
653
+
654
+ // Platform + Version
655
+ const pfIndex = pfItems.findIndex((item) =>
656
+ platformVersionReg.test(item)
657
+ );
658
+
659
+ if (pfIndex !== -1) {
660
+ const pfParts = pfItems[pfIndex].split(/\s+/);
661
+ platformVersion = pfParts.pop();
662
+ platform = pfParts.join(" ");
663
+ } else {
664
+ const appleVersionReg =
665
+ /((iPhone|Mac)\s+OS(\s+\w+)?)\s+((0|\d+)(_(0|\d+)){0,3})/i;
666
+
667
+ for (let i = 0; i < pfItems.length; i++) {
668
+ const match = appleVersionReg.exec(pfItems[i]);
669
+ if (match && match.length > 4) {
670
+ platform = match[1];
671
+ platformVersion = match[4].replace(/_/g, ".");
672
+
673
+ pfItems.splice(i, 1);
674
+ break;
675
+ }
723
676
  }
724
-
725
- if (versionReg.test(part)) {
726
- let [brand, version] = part.split('/');
727
- const pindex = version.indexOf('(');
728
- if (pindex > 0) {
729
- version = version.substring(0, pindex);
730
- }
731
- brands.push({
732
- brand,
733
- version: Utils.trimEnd(version, '.0')
734
- });
735
- return;
677
+ }
678
+
679
+ // Device
680
+ const deviceIndex = pfItems.findIndex((item) =>
681
+ item.includes(" Build/")
682
+ );
683
+ if (deviceIndex === -1) {
684
+ const firstItem = pfItems[0];
685
+ if (
686
+ firstItem.toLowerCase() !== "linux" &&
687
+ !firstItem.startsWith(platform)
688
+ ) {
689
+ device = firstItem;
690
+ pfItems.shift();
736
691
  }
692
+ } else {
693
+ device = pfItems[deviceIndex].split(" Build/")[0];
694
+ pfItems.splice(deviceIndex, 1);
695
+ }
696
+ }
697
+ return;
698
+ }
699
+
700
+ if (pl === "mobile" || pl.startsWith("mobile/")) {
701
+ mobile = true;
702
+ return;
703
+ }
704
+
705
+ if (pl === "version" || pl.startsWith("version/")) {
706
+ // No process
707
+ return;
708
+ }
709
+
710
+ if (versionReg.test(part)) {
711
+ let [brand, version] = part.split("/");
712
+ const pindex = version.indexOf("(");
713
+ if (pindex > 0) {
714
+ version = version.substring(0, pindex);
715
+ }
716
+ brands.push({
717
+ brand,
718
+ version: Utils.trimEnd(version, ".0")
737
719
  });
720
+ return;
721
+ }
722
+ });
738
723
 
739
- return { mobile, platform, platformVersion, brands, device };
740
- }
741
-
742
- /**
743
- * Set HTML element focus by name
744
- * @param name Element name or first collection item
745
- * @param container Container, limits the element range
746
- */
747
- export function setFocus(name: string | object, container?: HTMLElement) {
748
- const elementName =
749
- typeof name === 'string' ? name : Object.keys(name)[0];
750
-
751
- container ??= document.body;
752
-
753
- const element = container.querySelector<HTMLElement>(
754
- `[name="${elementName}"]`
755
- );
724
+ return { mobile, platform, platformVersion, brands, device };
725
+ }
726
+
727
+ /**
728
+ * Set HTML element focus by name
729
+ * @param name Element name or first collection item
730
+ * @param container Container, limits the element range
731
+ */
732
+ export function setFocus(name: string | object, container?: HTMLElement) {
733
+ const elementName = typeof name === "string" ? name : Object.keys(name)[0];
734
+
735
+ container ??= document.body;
736
+
737
+ const element = container.querySelector<HTMLElement>(
738
+ `[name="${elementName}"]`
739
+ );
740
+
741
+ if (element != null) element.focus();
742
+ }
743
+
744
+ /**
745
+ * Setup frontend logging
746
+ * @param action Logging action
747
+ * @param preventDefault Is prevent default action
748
+ * @param window Window object
749
+ */
750
+ export function setupLogging(
751
+ action: (data: ErrorData) => void | Promise<void>,
752
+ preventDefault?: ((type: ErrorType) => boolean) | boolean,
753
+ window: Window & typeof globalThis = globalThis.window
754
+ ) {
755
+ // Avoid multiple setup, if there is already a handler, please set "window.onunhandledrejection = null" first
756
+ if (window.onunhandledrejection) return;
757
+
758
+ const errorType: ErrorType = "error";
759
+ const errorPD = Utils.getResult(preventDefault, errorType) ?? true;
760
+ window.onerror = (message, source, lineNo, colNo, error) => {
761
+ // Default source
762
+ source ||= window.location.href;
763
+ let data: ErrorData;
764
+ if (typeof message === "string") {
765
+ data = {
766
+ type: errorType,
767
+ message, // Share the same message with error
768
+ source,
769
+ lineNo,
770
+ colNo,
771
+ stack: error?.stack
772
+ };
773
+ } else {
774
+ data = {
775
+ type: errorType,
776
+ subType: message.type,
777
+ message: error?.message ?? `${message.currentTarget} event error`,
778
+ source,
779
+ lineNo,
780
+ colNo,
781
+ stack: error?.stack
782
+ };
783
+ }
756
784
 
757
- if (element != null) element.focus();
758
- }
785
+ action(data);
759
786
 
760
- /**
761
- * Setup frontend logging
762
- * @param action Logging action
763
- * @param preventDefault Is prevent default action
764
- * @param window Window object
765
- */
766
- export function setupLogging(
767
- action: (data: ErrorData) => void | Promise<void>,
768
- preventDefault?: ((type: ErrorType) => boolean) | boolean,
769
- window: Window & typeof globalThis = globalThis.window
770
- ) {
771
- // Avoid multiple setup, if there is already a handler, please set "window.onunhandledrejection = null" first
772
- if (window.onunhandledrejection) return;
773
-
774
- const errorType: ErrorType = 'error';
775
- const errorPD = Utils.getResult(preventDefault, errorType) ?? true;
776
- window.onerror = (message, source, lineNo, colNo, error) => {
777
- // Default source
778
- source ||= window.location.href;
779
- let data: ErrorData;
780
- if (typeof message === 'string') {
781
- data = {
782
- type: errorType,
783
- message, // Share the same message with error
784
- source,
785
- lineNo,
786
- colNo,
787
- stack: error?.stack
788
- };
789
- } else {
790
- data = {
791
- type: errorType,
792
- subType: message.type,
793
- message:
794
- error?.message ??
795
- `${message.currentTarget} event error`,
796
- source,
797
- lineNo,
798
- colNo,
799
- stack: error?.stack
800
- };
801
- }
802
-
803
- action(data);
787
+ // Return true to suppress error alert
788
+ return errorPD;
789
+ };
804
790
 
805
- // Return true to suppress error alert
806
- return errorPD;
791
+ const rejectionType: ErrorType = "unhandledrejection";
792
+ const rejectionPD = Utils.getResult(preventDefault, rejectionType) ?? true;
793
+ window.onunhandledrejection = (event) => {
794
+ if (rejectionPD) event.preventDefault();
795
+
796
+ const reason = event.reason;
797
+ const source = window.location.href;
798
+ let data: ErrorData;
799
+
800
+ if (reason instanceof Error) {
801
+ const { name: subType, message, stack } = reason;
802
+ data = {
803
+ type: rejectionType,
804
+ subType,
805
+ message,
806
+ stack,
807
+ source
807
808
  };
809
+ } else {
810
+ data = {
811
+ type: rejectionType,
812
+ message: typeof reason === "string" ? reason : JSON.stringify(reason),
813
+ source
814
+ };
815
+ }
808
816
 
809
- const rejectionType: ErrorType = 'unhandledrejection';
810
- const rejectionPD =
811
- Utils.getResult(preventDefault, rejectionType) ?? true;
812
- window.onunhandledrejection = (event) => {
813
- if (rejectionPD) event.preventDefault();
814
-
815
- const reason = event.reason;
816
- const source = window.location.href;
817
- let data: ErrorData;
818
-
819
- if (reason instanceof Error) {
820
- const { name: subType, message, stack } = reason;
821
- data = {
822
- type: rejectionType,
823
- subType,
824
- message,
825
- stack,
826
- source
827
- };
828
- } else {
829
- data = {
830
- type: rejectionType,
831
- message:
832
- typeof reason === 'string'
833
- ? reason
834
- : JSON.stringify(reason),
835
- source
836
- };
837
- }
817
+ action(data);
818
+ };
838
819
 
839
- action(data);
840
- };
820
+ const localConsole = (
821
+ type: "consoleWarn" | "consoleError",
822
+ orgin: (...args: any[]) => void
823
+ ) => {
824
+ const consolePD = Utils.getResult(preventDefault, type) ?? false;
825
+ return (...args: any[]) => {
826
+ // Keep original action
827
+ if (!consolePD) orgin(...args);
828
+
829
+ const [first, ...rest] = args;
830
+ let message: string;
831
+ if (typeof first === "string") {
832
+ message = first;
833
+ } else {
834
+ message = JSON.stringify(first);
835
+ }
841
836
 
842
- const localConsole = (
843
- type: 'consoleWarn' | 'consoleError',
844
- orgin: (...args: any[]) => void
845
- ) => {
846
- const consolePD = Utils.getResult(preventDefault, type) ?? false;
847
- return (...args: any[]) => {
848
- // Keep original action
849
- if (!consolePD) orgin(...args);
850
-
851
- const [first, ...rest] = args;
852
- let message: string;
853
- if (typeof first === 'string') {
854
- message = first;
855
- } else {
856
- message = JSON.stringify(first);
857
- }
858
-
859
- const stack =
860
- rest.length > 0
861
- ? rest.map((item) => JSON.stringify(item)).join(', ')
862
- : undefined;
863
-
864
- const data: ErrorData = {
865
- type,
866
- message,
867
- source: window.location.href,
868
- stack
869
- };
870
-
871
- action(data);
872
- };
837
+ const stack =
838
+ rest.length > 0
839
+ ? rest.map((item) => JSON.stringify(item)).join(", ")
840
+ : undefined;
841
+
842
+ const data: ErrorData = {
843
+ type,
844
+ message,
845
+ source: window.location.href,
846
+ stack
873
847
  };
874
848
 
875
- window.console.warn = localConsole('consoleWarn', window.console.warn);
849
+ action(data);
850
+ };
851
+ };
876
852
 
877
- window.console.error = localConsole(
878
- 'consoleError',
879
- window.console.error
880
- );
853
+ window.console.warn = localConsole("consoleWarn", window.console.warn);
854
+
855
+ window.console.error = localConsole("consoleError", window.console.error);
856
+ }
857
+
858
+ /**
859
+ * Verify file system permission
860
+ * https://developer.mozilla.org/en-US/docs/Web/API/FileSystemHandle/requestPermission
861
+ * @param fileHandle FileSystemHandle
862
+ * @param withWrite With write permission
863
+ * @returns Result
864
+ */
865
+ export async function verifyPermission(
866
+ fileHandle: any,
867
+ withWrite: boolean = false
868
+ ) {
869
+ if (
870
+ !("queryPermission" in fileHandle) ||
871
+ !("requestPermission" in fileHandle)
872
+ )
873
+ return false;
874
+
875
+ // FileSystemHandlePermissionDescriptor
876
+ const opts = { mode: withWrite ? "readwrite" : "read" };
877
+
878
+ // Check if we already have permission, if so, return true.
879
+ if ((await fileHandle.queryPermission(opts)) === "granted") {
880
+ return true;
881
881
  }
882
882
 
883
- /**
884
- * Verify file system permission
885
- * https://developer.mozilla.org/en-US/docs/Web/API/FileSystemHandle/requestPermission
886
- * @param fileHandle FileSystemHandle
887
- * @param withWrite With write permission
888
- * @returns Result
889
- */
890
- export async function verifyPermission(
891
- fileHandle: any,
892
- withWrite: boolean = false
893
- ) {
894
- if (
895
- !('queryPermission' in fileHandle) ||
896
- !('requestPermission' in fileHandle)
897
- )
898
- return false;
899
-
900
- // FileSystemHandlePermissionDescriptor
901
- const opts = { mode: withWrite ? 'readwrite' : 'read' };
902
-
903
- // Check if we already have permission, if so, return true.
904
- if ((await fileHandle.queryPermission(opts)) === 'granted') {
905
- return true;
906
- }
907
-
908
- // Request permission to the file, if the user grants permission, return true.
909
- if ((await fileHandle.requestPermission(opts)) === 'granted') {
910
- return true;
911
- }
912
-
913
- // The user did not grant permission, return false.
914
- return false;
883
+ // Request permission to the file, if the user grants permission, return true.
884
+ if ((await fileHandle.requestPermission(opts)) === "granted") {
885
+ return true;
915
886
  }
887
+
888
+ // The user did not grant permission, return false.
889
+ return false;
890
+ }
916
891
  }