@etsoo/shared 1.2.40 → 1.2.42

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 CHANGED
@@ -224,8 +224,10 @@ DOM/window related utilities
224
224
  |headersToObject|Convert headers to object|
225
225
  |isFormData|Is IFormData type guard|
226
226
  |isJSONContentType|Is JSON content type|
227
+ |isWechatClient|Is Wechat client|
227
228
  |mergeFormData|Merge form data to primary one|
228
229
  |mergeURLSearchParams|Merge URL search parameters|
230
+ |parseUserAgent|parseUserAgent|
229
231
  |setFocus|Set HTML element focus by name|
230
232
  |setupLogging|Setup frontend logging|
231
233
  |verifyPermission|Verify file system permission|
@@ -328,6 +328,95 @@ test('Tests for getInputValue', () => {
328
328
  }
329
329
  });
330
330
 
331
+ test('Tests for getUserAgentData 1', () => {
332
+ const data = DomUtils.parseUserAgent(
333
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36'
334
+ );
335
+ expect(data?.device).toBe('Desktop');
336
+ expect(data?.platform).toBe('Windows NT');
337
+ expect(data?.platformVersion).toBe('10.0');
338
+ expect(data?.brands.find((b) => b.brand === 'Chrome')?.version).toBe('124');
339
+ });
340
+
341
+ test('Tests for getUserAgentData 2', () => {
342
+ const data = DomUtils.parseUserAgent(
343
+ 'Mozilla/5.0 (Linux; U; Android 2.3.6; zh-cn; GT-S5660 Build/GINGERBREAD) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1 MicroMessenger/4.5.255'
344
+ );
345
+ expect(data?.device).toBe('GT-S5660');
346
+ expect(data?.platform).toBe('Android');
347
+ expect(data?.platformVersion).toBe('2.3.6');
348
+ expect(data?.mobile).toBeTruthy();
349
+ expect(DomUtils.isWechatClient(data)).toBeTruthy();
350
+ });
351
+
352
+ test('Tests for getUserAgentData 3', () => {
353
+ const data = DomUtils.parseUserAgent(
354
+ 'Mozilla/5.0 (Linux; Android 7.1.1;MEIZU E3 Build/NGI77B; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/66.0.3359.126 MQQBrowser/9.6 TBS/044428 Mobile Safari/537.36 MicroMessenger/6.6.7.1321(0x26060739) NetType/WIFI Language/zh_CN'
355
+ );
356
+
357
+ expect(data?.device).toBe('MEIZU E3');
358
+ expect(data?.platform).toBe('Android');
359
+ expect(data?.platformVersion).toBe('7.1.1');
360
+ expect(data?.mobile).toBeTruthy();
361
+ expect(DomUtils.isWechatClient(data)).toBeTruthy();
362
+ });
363
+
364
+ test('Tests for getUserAgentData 4', () => {
365
+ const data = DomUtils.parseUserAgent(
366
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4.1 Mobile/15E148 Safari/604.1'
367
+ );
368
+
369
+ expect(data?.device).toBe('iPhone');
370
+ expect(data?.platform).toBe('iPhone OS');
371
+ expect(data?.platformVersion).toBe('17.5.1');
372
+ expect(data?.mobile).toBeTruthy();
373
+ expect(DomUtils.isWechatClient(data)).toBeFalsy();
374
+ });
375
+
376
+ test('Tests for getUserAgentData 5', () => {
377
+ const data = DomUtils.parseUserAgent(
378
+ 'Mozilla/5.0 (SMART-TV; Linux; Tizen 2.3) AppleWebkit/538.1 (KHTML, like Gecko) SamsungBrowser/1.0 TV Safari/538.1'
379
+ );
380
+
381
+ expect(data?.device).toBe('SMART-TV');
382
+ expect(data?.platform).toBe('Tizen');
383
+ expect(data?.platformVersion).toBe('2.3');
384
+ expect(data?.mobile).toBeFalsy();
385
+ });
386
+
387
+ test('Tests for getUserAgentData 6', () => {
388
+ const data = DomUtils.parseUserAgent(
389
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0 Safari/605.1.15'
390
+ );
391
+
392
+ expect(data?.device).toBe('Macintosh');
393
+ expect(data?.platform).toBe('Mac OS X');
394
+ expect(data?.platformVersion).toBe('10.15');
395
+ expect(data?.mobile).toBeFalsy();
396
+ });
397
+
398
+ test('Tests for getUserAgentData 7', () => {
399
+ const data = DomUtils.parseUserAgent(
400
+ 'Mozilla/5.0 (Linux; Android 8.1; LEO-DLXXE Build/HONORLRA-AL00) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.111 HuaweiBrowser/9.1.1.308 Mobile Safari/537.36'
401
+ );
402
+
403
+ expect(data?.device).toBe('LEO-DLXXE');
404
+ expect(data?.platform).toBe('Android');
405
+ expect(data?.platformVersion).toBe('8.1');
406
+ expect(data?.mobile).toBeTruthy();
407
+ });
408
+
409
+ test('Tests for getUserAgentData 8', () => {
410
+ const data = DomUtils.parseUserAgent(
411
+ 'Mozilla/5.0 (Linux; Android 9; SM-R825F Build/QP1A.190711.020; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/80.0.3987.119 Mobile Safari/537.36'
412
+ );
413
+
414
+ expect(data?.device).toBe('SM-R825F');
415
+ expect(data?.platform).toBe('Android');
416
+ expect(data?.platformVersion).toBe('9');
417
+ expect(data?.mobile).toBeTruthy();
418
+ });
419
+
331
420
  test('Tests for setupLogging', async () => {
332
421
  // Arrange
333
422
  const action = jest.fn((data: ErrorData) => {
@@ -306,6 +306,16 @@ test('Tests for setNestedValue', () => {
306
306
  expect(Reflect.get((obj.jsonData as any).newProperty, 'value')).toBe(125);
307
307
  });
308
308
 
309
+ test('Tests for setNestedValue removal', () => {
310
+ const obj = { jsonData: { photoSize: [200, 100], supportResizing: true } };
311
+
312
+ Utils.setNestedValue(obj, 'jsonData.photoSize', undefined);
313
+ expect(obj.jsonData.photoSize).toBeUndefined();
314
+
315
+ Utils.setNestedValue(obj, 'jsonData.supportResizing', undefined);
316
+ expect(obj.jsonData.supportResizing).toBeUndefined();
317
+ });
318
+
309
319
  test('Tests for sortByFavor', () => {
310
320
  const items = [1, 2, 3, 4, 5, 6, 7];
311
321
  expect(Utils.sortByFavor(items, [5, 1, 3])).toStrictEqual([
@@ -345,4 +355,5 @@ test('Tests for trimEnd', () => {
345
355
  expect(Utils.trimEnd('//a/', '/')).toBe('//a');
346
356
  expect(Utils.trimEnd('/*/a*/', ...['/', '*'])).toBe('/*/a');
347
357
  expect(Utils.trimEnd('abc', ...['/', '*'])).toBe('abc');
358
+ expect(Utils.trimEnd('12.0.0.0', '.0')).toBe('12');
348
359
  });
@@ -2,6 +2,35 @@
2
2
  import { DataTypes } from './DataTypes';
3
3
  import { ErrorData, ErrorType } from './types/ErrorData';
4
4
  import { FormDataFieldValue, IFormData } from './types/FormData';
5
+ /**
6
+ * User agent data, maybe replaced by navigator.userAgentData in future
7
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/Navigator/userAgentData
8
+ */
9
+ export type UserAgentData = {
10
+ /**
11
+ * Browser brands
12
+ */
13
+ brands: {
14
+ brand: string;
15
+ version: string;
16
+ }[];
17
+ /**
18
+ * Is mobile device
19
+ */
20
+ mobile: boolean;
21
+ /**
22
+ * Device brand (name)
23
+ */
24
+ device: string;
25
+ /**
26
+ * Platform (OS)
27
+ */
28
+ platform: string;
29
+ /**
30
+ * Platform version
31
+ */
32
+ platformVersion?: string;
33
+ };
5
34
  /**
6
35
  * Dom Utilities
7
36
  * Not all methods support Node
@@ -71,6 +100,12 @@ export declare namespace DomUtils {
71
100
  * @returns Object
72
101
  */
73
102
  function formDataToObject(form: IFormData): Record<string, FormDataFieldValue | FormDataFieldValue[]>;
103
+ /**
104
+ * Is wechat client
105
+ * @param data User agent data
106
+ * @returns Result
107
+ */
108
+ function isWechatClient(data?: UserAgentData | null): boolean;
74
109
  /**
75
110
  * Culture match case Enum
76
111
  */
@@ -144,6 +179,14 @@ export declare namespace DomUtils {
144
179
  * @param data New simple object data to merge
145
180
  */
146
181
  function mergeURLSearchParams(base: URLSearchParams, data: DataTypes.SimpleObject): URLSearchParams;
182
+ /**
183
+ * Parse navigator's user agent string
184
+ * Lightweight User-Agent string parser
185
+ * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent
186
+ * @param ua User agent string
187
+ * @returns User agent data
188
+ */
189
+ function parseUserAgent(ua?: string): UserAgentData | null;
147
190
  /**
148
191
  * Set HTML element focus by name
149
192
  * @param name Element name or first collection item
@@ -307,6 +307,18 @@ var DomUtils;
307
307
  return dic;
308
308
  }
309
309
  DomUtils.formDataToObject = formDataToObject;
310
+ /**
311
+ * Is wechat client
312
+ * @param data User agent data
313
+ * @returns Result
314
+ */
315
+ function isWechatClient(data) {
316
+ data ?? (data = parseUserAgent());
317
+ if (!data)
318
+ return false;
319
+ return data.brands.some((item) => item.brand.toLowerCase() === 'micromessenger');
320
+ }
321
+ DomUtils.isWechatClient = isWechatClient;
310
322
  /**
311
323
  * Culture match case Enum
312
324
  */
@@ -483,6 +495,93 @@ var DomUtils;
483
495
  return base;
484
496
  }
485
497
  DomUtils.mergeURLSearchParams = mergeURLSearchParams;
498
+ /**
499
+ * Parse navigator's user agent string
500
+ * Lightweight User-Agent string parser
501
+ * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent
502
+ * @param ua User agent string
503
+ * @returns User agent data
504
+ */
505
+ function parseUserAgent(ua) {
506
+ ua ?? (ua = globalThis.navigator.userAgent);
507
+ if (!ua) {
508
+ return null;
509
+ }
510
+ const parts = ua.split(/(?!\(.*)\s+(?!\()(?![^(]*?\))/g);
511
+ let mobile = false;
512
+ let platform = '';
513
+ let platformVersion;
514
+ let device = 'Desktop';
515
+ const brands = [];
516
+ // with the 'g' will causing failures for multiple calls
517
+ const platformVersionReg = /^[a-zA-Z0-9-\s]+\s+(0|\d+)(\.(0|\d+)){0,3}(\(|$)/;
518
+ const versionReg = /^[a-zA-Z0-9]+\/(0|\d+)(\.(0|\d+)){0,3}(\(|$)/;
519
+ parts.forEach((part) => {
520
+ const pl = part.toLowerCase();
521
+ if (pl.startsWith('mozilla/')) {
522
+ const data = /\((.*)\)$/.exec(part);
523
+ if (data && data.length > 1) {
524
+ const pfItems = data[1].split(/;\s*/);
525
+ // Platform + Version
526
+ const pfIndex = pfItems.findIndex((item) => platformVersionReg.test(item));
527
+ if (pfIndex !== -1) {
528
+ const pfParts = pfItems[pfIndex].split(/\s+/);
529
+ platformVersion = pfParts.pop();
530
+ platform = pfParts.join(' ');
531
+ }
532
+ else {
533
+ const appleVersionReg = /((iPhone|Mac)\s+OS(\s+\w+)?)\s+((0|\d+)(_(0|\d+)){0,3})/i;
534
+ for (let i = 0; i < pfItems.length; i++) {
535
+ const match = appleVersionReg.exec(pfItems[i]);
536
+ if (match && match.length > 4) {
537
+ platform = match[1];
538
+ platformVersion = match[4].replace(/_/g, '.');
539
+ pfItems.splice(i, 1);
540
+ break;
541
+ }
542
+ }
543
+ }
544
+ // Device
545
+ const deviceIndex = pfItems.findIndex((item) => item.includes(' Build/'));
546
+ if (deviceIndex === -1) {
547
+ const firstItem = pfItems[0];
548
+ if (firstItem.toLowerCase() !== 'linux' &&
549
+ !firstItem.startsWith(platform)) {
550
+ device = firstItem;
551
+ pfItems.shift();
552
+ }
553
+ }
554
+ else {
555
+ device = pfItems[deviceIndex].split(' Build/')[0];
556
+ pfItems.splice(deviceIndex, 1);
557
+ }
558
+ }
559
+ return;
560
+ }
561
+ if (pl === 'mobile' || pl.startsWith('mobile/')) {
562
+ mobile = true;
563
+ return;
564
+ }
565
+ if (pl === 'version' || pl.startsWith('version/')) {
566
+ // No process
567
+ return;
568
+ }
569
+ if (versionReg.test(part)) {
570
+ let [brand, version] = part.split('/');
571
+ const pindex = version.indexOf('(');
572
+ if (pindex > 0) {
573
+ version = version.substring(0, pindex);
574
+ }
575
+ brands.push({
576
+ brand,
577
+ version: Utils_1.Utils.trimEnd(version, '.0')
578
+ });
579
+ return;
580
+ }
581
+ });
582
+ return { mobile, platform, platformVersion, brands, device };
583
+ }
584
+ DomUtils.parseUserAgent = parseUserAgent;
486
585
  /**
487
586
  * Set HTML element focus by name
488
587
  * @param name Element name or first collection item
@@ -267,8 +267,9 @@ export declare namespace Utils {
267
267
  * @param data Data
268
268
  * @param name Field name, support property chain like 'jsonData.logSize'
269
269
  * @param value Value
270
+ * @param keepNull Keep null value or not
270
271
  */
271
- function setNestedValue(data: object, name: string, value: unknown): void;
272
+ function setNestedValue(data: object, name: string, value: unknown, keepNull?: boolean): void;
272
273
  /**
273
274
  * Parse path similar with node.js path.parse
274
275
  * @param path Input path
package/lib/cjs/Utils.js CHANGED
@@ -541,18 +541,26 @@ var Utils;
541
541
  * @param data Data
542
542
  * @param name Field name, support property chain like 'jsonData.logSize'
543
543
  * @param value Value
544
+ * @param keepNull Keep null value or not
544
545
  */
545
- function setNestedValue(data, name, value) {
546
+ function setNestedValue(data, name, value, keepNull) {
546
547
  const properties = name.split('.');
547
548
  const len = properties.length;
548
- if (len === 1)
549
- Reflect.set(data, name, value);
549
+ if (len === 1) {
550
+ if (value == null && keepNull !== true) {
551
+ Reflect.deleteProperty(data, name);
552
+ }
553
+ else {
554
+ Reflect.set(data, name, value);
555
+ }
556
+ }
550
557
  else {
551
558
  let curr = data;
552
559
  for (let i = 0; i < len; i++) {
553
560
  const property = properties[i];
554
561
  if (i + 1 === len) {
555
- Reflect.set(curr, property, value);
562
+ setNestedValue(curr, property, value, keepNull);
563
+ // Reflect.set(curr, property, value);
556
564
  }
557
565
  else {
558
566
  let p = Reflect.get(curr, property);
@@ -647,10 +655,11 @@ var Utils;
647
655
  * @returns Result
648
656
  */
649
657
  Utils.trimEnd = (input, ...chars) => {
650
- let start = input.length - 1;
651
- while (start >= 0 && chars.indexOf(input[start]) >= 0)
652
- --start;
653
- return input.substring(0, start + 1);
658
+ let char;
659
+ while ((char = chars.find((char) => input.endsWith(char))) != null) {
660
+ input = input.substring(0, input.length - char.length);
661
+ }
662
+ return input;
654
663
  };
655
664
  /**
656
665
  * Trim start chars
@@ -659,10 +668,10 @@ var Utils;
659
668
  * @returns Result
660
669
  */
661
670
  Utils.trimStart = (input, ...chars) => {
662
- let start = 0;
663
- const end = input.length;
664
- while (start < end && chars.indexOf(input[start]) >= 0)
665
- ++start;
666
- return input.substring(start);
671
+ let char;
672
+ while ((char = chars.find((char) => input.startsWith(char))) != null) {
673
+ input = input.substring(char.length);
674
+ }
675
+ return input;
667
676
  };
668
677
  })(Utils || (exports.Utils = Utils = {}));
@@ -2,6 +2,35 @@
2
2
  import { DataTypes } from './DataTypes';
3
3
  import { ErrorData, ErrorType } from './types/ErrorData';
4
4
  import { FormDataFieldValue, IFormData } from './types/FormData';
5
+ /**
6
+ * User agent data, maybe replaced by navigator.userAgentData in future
7
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/Navigator/userAgentData
8
+ */
9
+ export type UserAgentData = {
10
+ /**
11
+ * Browser brands
12
+ */
13
+ brands: {
14
+ brand: string;
15
+ version: string;
16
+ }[];
17
+ /**
18
+ * Is mobile device
19
+ */
20
+ mobile: boolean;
21
+ /**
22
+ * Device brand (name)
23
+ */
24
+ device: string;
25
+ /**
26
+ * Platform (OS)
27
+ */
28
+ platform: string;
29
+ /**
30
+ * Platform version
31
+ */
32
+ platformVersion?: string;
33
+ };
5
34
  /**
6
35
  * Dom Utilities
7
36
  * Not all methods support Node
@@ -71,6 +100,12 @@ export declare namespace DomUtils {
71
100
  * @returns Object
72
101
  */
73
102
  function formDataToObject(form: IFormData): Record<string, FormDataFieldValue | FormDataFieldValue[]>;
103
+ /**
104
+ * Is wechat client
105
+ * @param data User agent data
106
+ * @returns Result
107
+ */
108
+ function isWechatClient(data?: UserAgentData | null): boolean;
74
109
  /**
75
110
  * Culture match case Enum
76
111
  */
@@ -144,6 +179,14 @@ export declare namespace DomUtils {
144
179
  * @param data New simple object data to merge
145
180
  */
146
181
  function mergeURLSearchParams(base: URLSearchParams, data: DataTypes.SimpleObject): URLSearchParams;
182
+ /**
183
+ * Parse navigator's user agent string
184
+ * Lightweight User-Agent string parser
185
+ * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent
186
+ * @param ua User agent string
187
+ * @returns User agent data
188
+ */
189
+ function parseUserAgent(ua?: string): UserAgentData | null;
147
190
  /**
148
191
  * Set HTML element focus by name
149
192
  * @param name Element name or first collection item
@@ -304,6 +304,18 @@ export var DomUtils;
304
304
  return dic;
305
305
  }
306
306
  DomUtils.formDataToObject = formDataToObject;
307
+ /**
308
+ * Is wechat client
309
+ * @param data User agent data
310
+ * @returns Result
311
+ */
312
+ function isWechatClient(data) {
313
+ data ?? (data = parseUserAgent());
314
+ if (!data)
315
+ return false;
316
+ return data.brands.some((item) => item.brand.toLowerCase() === 'micromessenger');
317
+ }
318
+ DomUtils.isWechatClient = isWechatClient;
307
319
  /**
308
320
  * Culture match case Enum
309
321
  */
@@ -480,6 +492,93 @@ export var DomUtils;
480
492
  return base;
481
493
  }
482
494
  DomUtils.mergeURLSearchParams = mergeURLSearchParams;
495
+ /**
496
+ * Parse navigator's user agent string
497
+ * Lightweight User-Agent string parser
498
+ * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent
499
+ * @param ua User agent string
500
+ * @returns User agent data
501
+ */
502
+ function parseUserAgent(ua) {
503
+ ua ?? (ua = globalThis.navigator.userAgent);
504
+ if (!ua) {
505
+ return null;
506
+ }
507
+ const parts = ua.split(/(?!\(.*)\s+(?!\()(?![^(]*?\))/g);
508
+ let mobile = false;
509
+ let platform = '';
510
+ let platformVersion;
511
+ let device = 'Desktop';
512
+ const brands = [];
513
+ // with the 'g' will causing failures for multiple calls
514
+ const platformVersionReg = /^[a-zA-Z0-9-\s]+\s+(0|\d+)(\.(0|\d+)){0,3}(\(|$)/;
515
+ const versionReg = /^[a-zA-Z0-9]+\/(0|\d+)(\.(0|\d+)){0,3}(\(|$)/;
516
+ parts.forEach((part) => {
517
+ const pl = part.toLowerCase();
518
+ if (pl.startsWith('mozilla/')) {
519
+ const data = /\((.*)\)$/.exec(part);
520
+ if (data && data.length > 1) {
521
+ const pfItems = data[1].split(/;\s*/);
522
+ // Platform + Version
523
+ const pfIndex = pfItems.findIndex((item) => platformVersionReg.test(item));
524
+ if (pfIndex !== -1) {
525
+ const pfParts = pfItems[pfIndex].split(/\s+/);
526
+ platformVersion = pfParts.pop();
527
+ platform = pfParts.join(' ');
528
+ }
529
+ else {
530
+ const appleVersionReg = /((iPhone|Mac)\s+OS(\s+\w+)?)\s+((0|\d+)(_(0|\d+)){0,3})/i;
531
+ for (let i = 0; i < pfItems.length; i++) {
532
+ const match = appleVersionReg.exec(pfItems[i]);
533
+ if (match && match.length > 4) {
534
+ platform = match[1];
535
+ platformVersion = match[4].replace(/_/g, '.');
536
+ pfItems.splice(i, 1);
537
+ break;
538
+ }
539
+ }
540
+ }
541
+ // Device
542
+ const deviceIndex = pfItems.findIndex((item) => item.includes(' Build/'));
543
+ if (deviceIndex === -1) {
544
+ const firstItem = pfItems[0];
545
+ if (firstItem.toLowerCase() !== 'linux' &&
546
+ !firstItem.startsWith(platform)) {
547
+ device = firstItem;
548
+ pfItems.shift();
549
+ }
550
+ }
551
+ else {
552
+ device = pfItems[deviceIndex].split(' Build/')[0];
553
+ pfItems.splice(deviceIndex, 1);
554
+ }
555
+ }
556
+ return;
557
+ }
558
+ if (pl === 'mobile' || pl.startsWith('mobile/')) {
559
+ mobile = true;
560
+ return;
561
+ }
562
+ if (pl === 'version' || pl.startsWith('version/')) {
563
+ // No process
564
+ return;
565
+ }
566
+ if (versionReg.test(part)) {
567
+ let [brand, version] = part.split('/');
568
+ const pindex = version.indexOf('(');
569
+ if (pindex > 0) {
570
+ version = version.substring(0, pindex);
571
+ }
572
+ brands.push({
573
+ brand,
574
+ version: Utils.trimEnd(version, '.0')
575
+ });
576
+ return;
577
+ }
578
+ });
579
+ return { mobile, platform, platformVersion, brands, device };
580
+ }
581
+ DomUtils.parseUserAgent = parseUserAgent;
483
582
  /**
484
583
  * Set HTML element focus by name
485
584
  * @param name Element name or first collection item
@@ -267,8 +267,9 @@ export declare namespace Utils {
267
267
  * @param data Data
268
268
  * @param name Field name, support property chain like 'jsonData.logSize'
269
269
  * @param value Value
270
+ * @param keepNull Keep null value or not
270
271
  */
271
- function setNestedValue(data: object, name: string, value: unknown): void;
272
+ function setNestedValue(data: object, name: string, value: unknown, keepNull?: boolean): void;
272
273
  /**
273
274
  * Parse path similar with node.js path.parse
274
275
  * @param path Input path
package/lib/mjs/Utils.js CHANGED
@@ -535,18 +535,26 @@ export var Utils;
535
535
  * @param data Data
536
536
  * @param name Field name, support property chain like 'jsonData.logSize'
537
537
  * @param value Value
538
+ * @param keepNull Keep null value or not
538
539
  */
539
- function setNestedValue(data, name, value) {
540
+ function setNestedValue(data, name, value, keepNull) {
540
541
  const properties = name.split('.');
541
542
  const len = properties.length;
542
- if (len === 1)
543
- Reflect.set(data, name, value);
543
+ if (len === 1) {
544
+ if (value == null && keepNull !== true) {
545
+ Reflect.deleteProperty(data, name);
546
+ }
547
+ else {
548
+ Reflect.set(data, name, value);
549
+ }
550
+ }
544
551
  else {
545
552
  let curr = data;
546
553
  for (let i = 0; i < len; i++) {
547
554
  const property = properties[i];
548
555
  if (i + 1 === len) {
549
- Reflect.set(curr, property, value);
556
+ setNestedValue(curr, property, value, keepNull);
557
+ // Reflect.set(curr, property, value);
550
558
  }
551
559
  else {
552
560
  let p = Reflect.get(curr, property);
@@ -641,10 +649,11 @@ export var Utils;
641
649
  * @returns Result
642
650
  */
643
651
  Utils.trimEnd = (input, ...chars) => {
644
- let start = input.length - 1;
645
- while (start >= 0 && chars.indexOf(input[start]) >= 0)
646
- --start;
647
- return input.substring(0, start + 1);
652
+ let char;
653
+ while ((char = chars.find((char) => input.endsWith(char))) != null) {
654
+ input = input.substring(0, input.length - char.length);
655
+ }
656
+ return input;
648
657
  };
649
658
  /**
650
659
  * Trim start chars
@@ -653,10 +662,10 @@ export var Utils;
653
662
  * @returns Result
654
663
  */
655
664
  Utils.trimStart = (input, ...chars) => {
656
- let start = 0;
657
- const end = input.length;
658
- while (start < end && chars.indexOf(input[start]) >= 0)
659
- ++start;
660
- return input.substring(start);
665
+ let char;
666
+ while ((char = chars.find((char) => input.startsWith(char))) != null) {
667
+ input = input.substring(char.length);
668
+ }
669
+ return input;
661
670
  };
662
671
  })(Utils || (Utils = {}));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@etsoo/shared",
3
- "version": "1.2.40",
3
+ "version": "1.2.42",
4
4
  "description": "TypeScript shared utilities and functions",
5
5
  "main": "lib/cjs/index.js",
6
6
  "module": "lib/mjs/index.js",
@@ -58,7 +58,7 @@
58
58
  "@types/lodash.isequal": "^4.5.8",
59
59
  "jest": "^29.7.0",
60
60
  "jest-environment-jsdom": "^29.7.0",
61
- "ts-jest": "^29.1.2",
61
+ "ts-jest": "^29.1.4",
62
62
  "typescript": "^5.4.5"
63
63
  },
64
64
  "dependencies": {
package/src/DomUtils.ts CHANGED
@@ -11,6 +11,40 @@ if (typeof navigator === 'undefined') {
11
11
  globalThis.location = { href: 'http://localhost/' } as any;
12
12
  }
13
13
 
14
+ /**
15
+ * User agent data, maybe replaced by navigator.userAgentData in future
16
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/Navigator/userAgentData
17
+ */
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;
46
+ };
47
+
14
48
  /**
15
49
  * Dom Utilities
16
50
  * Not all methods support Node
@@ -367,6 +401,20 @@ export namespace DomUtils {
367
401
  return dic;
368
402
  }
369
403
 
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
+ );
416
+ }
417
+
370
418
  /**
371
419
  * Culture match case Enum
372
420
  */
@@ -583,6 +631,114 @@ export namespace DomUtils {
583
631
  return base;
584
632
  }
585
633
 
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
+ }
719
+
720
+ if (pl === 'version' || pl.startsWith('version/')) {
721
+ // No process
722
+ return;
723
+ }
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;
736
+ }
737
+ });
738
+
739
+ return { mobile, platform, platformVersion, brands, device };
740
+ }
741
+
586
742
  /**
587
743
  * Set HTML element focus by name
588
744
  * @param name Element name or first collection item
package/src/Utils.ts CHANGED
@@ -735,18 +735,30 @@ export namespace Utils {
735
735
  * @param data Data
736
736
  * @param name Field name, support property chain like 'jsonData.logSize'
737
737
  * @param value Value
738
+ * @param keepNull Keep null value or not
738
739
  */
739
- export function setNestedValue(data: object, name: string, value: unknown) {
740
+ export function setNestedValue(
741
+ data: object,
742
+ name: string,
743
+ value: unknown,
744
+ keepNull?: boolean
745
+ ) {
740
746
  const properties = name.split('.');
741
747
  const len = properties.length;
742
- if (len === 1) Reflect.set(data, name, value);
743
- else {
748
+ if (len === 1) {
749
+ if (value == null && keepNull !== true) {
750
+ Reflect.deleteProperty(data, name);
751
+ } else {
752
+ Reflect.set(data, name, value);
753
+ }
754
+ } else {
744
755
  let curr = data;
745
756
  for (let i = 0; i < len; i++) {
746
757
  const property = properties[i];
747
758
 
748
759
  if (i + 1 === len) {
749
- Reflect.set(curr, property, value);
760
+ setNestedValue(curr, property, value, keepNull);
761
+ // Reflect.set(curr, property, value);
750
762
  } else {
751
763
  let p = Reflect.get(curr, property);
752
764
  if (p == null) {
@@ -858,11 +870,11 @@ export namespace Utils {
858
870
  * @returns Result
859
871
  */
860
872
  export const trimEnd = (input: string, ...chars: string[]) => {
861
- let start = input.length - 1;
862
-
863
- while (start >= 0 && chars.indexOf(input[start]) >= 0) --start;
864
-
865
- return input.substring(0, start + 1);
873
+ let char: string | undefined;
874
+ while ((char = chars.find((char) => input.endsWith(char))) != null) {
875
+ input = input.substring(0, input.length - char.length);
876
+ }
877
+ return input;
866
878
  };
867
879
 
868
880
  /**
@@ -872,11 +884,10 @@ export namespace Utils {
872
884
  * @returns Result
873
885
  */
874
886
  export const trimStart = (input: string, ...chars: string[]) => {
875
- let start = 0;
876
- const end = input.length;
877
-
878
- while (start < end && chars.indexOf(input[start]) >= 0) ++start;
879
-
880
- return input.substring(start);
887
+ let char: string | undefined;
888
+ while ((char = chars.find((char) => input.startsWith(char))) != null) {
889
+ input = input.substring(char.length);
890
+ }
891
+ return input;
881
892
  };
882
893
  }