@uxf/core 11.123.0 → 11.126.0
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 +142 -106
- package/package.json +1 -1
- package/utils/safe-storage.d.ts +10 -0
- package/utils/safe-storage.js +108 -0
- package/utils/safe-storage.test.d.ts +1 -0
- package/utils/safe-storage.test.js +57 -0
package/README.md
CHANGED
|
@@ -3,24 +3,26 @@
|
|
|
3
3
|
## Constants
|
|
4
4
|
|
|
5
5
|
- common modifier classnames for interactive elements (eg. `CLASSES.IS_HOVERABLE` for is-hoverable classname)
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
6
|
+
- `focus-visible`
|
|
7
|
+
- `is-active`
|
|
8
|
+
- `is-busy`
|
|
9
|
+
- `is-disabled`
|
|
10
|
+
- `is-focused`
|
|
11
|
+
- `is-hoverable`
|
|
12
|
+
- `is-hovered`
|
|
13
|
+
- `is-invalid`
|
|
14
|
+
- `is-loading`
|
|
15
|
+
- `is-not-hoverable`
|
|
16
|
+
- `is-readonly`
|
|
17
|
+
- `is-required`
|
|
18
|
+
- `is-selected`
|
|
19
19
|
|
|
20
20
|
## Resizer
|
|
21
|
+
|
|
21
22
|
!!! Required [@uxf/resizer](https://www.npmjs.com/package/@uxf/resizer) version `>= 2.3.2` which supported `quality` parameter.
|
|
22
23
|
|
|
23
24
|
### Config
|
|
25
|
+
|
|
24
26
|
```json
|
|
25
27
|
[
|
|
26
28
|
{
|
|
@@ -37,19 +39,19 @@
|
|
|
37
39
|
### Usage for generated images
|
|
38
40
|
|
|
39
41
|
```tsx
|
|
40
|
-
import {resizerImageUrl} from "@uxf/core/utils/resizer";
|
|
42
|
+
import { resizerImageUrl } from "@uxf/core/utils/resizer";
|
|
41
43
|
|
|
42
|
-
<img src={resizerImageUrl(file, width, height, params)}
|
|
44
|
+
<img src={resizerImageUrl(file, width, height, params)} />;
|
|
43
45
|
```
|
|
44
46
|
|
|
45
47
|
### Usage for static images
|
|
46
48
|
|
|
47
49
|
```tsx
|
|
48
|
-
import {resizerImageUrl} from "@uxf/core/utils/resizer";
|
|
50
|
+
import { resizerImageUrl } from "@uxf/core/utils/resizer";
|
|
49
51
|
|
|
50
52
|
import staticImage from "./path/to/static-image.png";
|
|
51
53
|
|
|
52
|
-
<img src={resizerImageUrl(staticImage, width, height, params)}
|
|
54
|
+
<img src={resizerImageUrl(staticImage, width, height, params)} />;
|
|
53
55
|
```
|
|
54
56
|
|
|
55
57
|
## QR code generator
|
|
@@ -93,43 +95,49 @@ cookie.delete("cookie-name", /* options (optional) */);
|
|
|
93
95
|
Dynamically adjusts the height of a `<textarea>` based on its content and an optional number of rows.
|
|
94
96
|
|
|
95
97
|
#### Parameters
|
|
98
|
+
|
|
96
99
|
- **`element`**: The `<textarea>` to adjust.
|
|
97
100
|
- **`rows`** (optional): Minimum visible rows. Default is `4`.
|
|
98
101
|
|
|
99
102
|
#### Behavior
|
|
103
|
+
|
|
100
104
|
- Leverages MutationObserver API to measure content height.
|
|
101
105
|
- Adjusts height to fit content or the minimum height based on the `rows` parameter, calculated using `line-height` and `font-size`.
|
|
102
106
|
|
|
103
107
|
#### Usage
|
|
108
|
+
|
|
104
109
|
```typescript
|
|
105
110
|
adjustTextareaHeight(textarea); // Adjusts height (min 4 rows)
|
|
106
111
|
adjustTextareaHeight(textarea, 6); // With 6-row minimum
|
|
107
|
-
```
|
|
112
|
+
```
|
|
113
|
+
|
|
108
114
|
In React component:
|
|
115
|
+
|
|
109
116
|
```tsx
|
|
110
117
|
import { useIsomorphicLayoutEffect } from "@uxf/core-react/hooks/use-isomorphic-layout-effect";
|
|
111
118
|
import { isNotNil } from "@uxf/core/utils/is-not-nil";
|
|
112
119
|
|
|
113
120
|
useIsomorphicLayoutEffect(() => {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
const textarea = textareaRef.current;
|
|
122
|
+
|
|
123
|
+
if (isNotNil(textarea)) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const cleanup = adjustTextareaHeight(textarea);
|
|
128
|
+
|
|
129
|
+
return () => cleanup();
|
|
123
130
|
}, []);
|
|
124
|
-
```
|
|
131
|
+
```
|
|
125
132
|
|
|
126
133
|
> **Note**: Requires valid `line-height` and `font-size` styles for accurate sizing.
|
|
127
134
|
|
|
128
135
|
### assertNever
|
|
129
136
|
|
|
130
137
|
Checks that value is always type "never".
|
|
138
|
+
|
|
131
139
|
```ts
|
|
132
|
-
switch(value) {
|
|
140
|
+
switch (value) {
|
|
133
141
|
case "a":
|
|
134
142
|
return "A";
|
|
135
143
|
case "b":
|
|
@@ -209,9 +217,11 @@ const example = <div ref={composeRefs(firstRef, secondRef)} />;
|
|
|
209
217
|
```
|
|
210
218
|
|
|
211
219
|
### cx, cxa
|
|
220
|
+
|
|
212
221
|
It is our fork of `clsx` library https://github.com/lukeed/clsx
|
|
213
222
|
|
|
214
223
|
We will mainly use `cx`, which is fork of `clsx/lite` – it accepts **ONLY** string values! Any non-string arguments are ignored!
|
|
224
|
+
|
|
215
225
|
```tsx
|
|
216
226
|
import { cx } from "@uxf/core/utils/cx";
|
|
217
227
|
|
|
@@ -224,9 +234,10 @@ cx({ foo: true });
|
|
|
224
234
|
//=> ""
|
|
225
235
|
```
|
|
226
236
|
|
|
227
|
-
The `cxa` function is full fork of `clsx` and can take
|
|
237
|
+
The `cxa` function is full fork of `clsx` and can take _any_ number of arguments, each of which can be an Object, Array, Boolean, or String.
|
|
228
238
|
|
|
229
239
|
**Important**: Any falsy values are discarded! Standalone Boolean values are discarded as well.
|
|
240
|
+
|
|
230
241
|
```tsx
|
|
231
242
|
import { cxa } from "@uxf/core/utils/cxa";
|
|
232
243
|
|
|
@@ -238,11 +249,11 @@ cxa("foo", true && "bar", "baz");
|
|
|
238
249
|
//=> "foo bar baz"
|
|
239
250
|
|
|
240
251
|
// Objects
|
|
241
|
-
cxa({ foo:true, bar:false, baz:isTrue() });
|
|
252
|
+
cxa({ foo: true, bar: false, baz: isTrue() });
|
|
242
253
|
//=> "foo baz"
|
|
243
254
|
|
|
244
255
|
// Objects (variadic)
|
|
245
|
-
cxa({ foo:true }, { bar:false }, null, { "--foobar":"hello" });
|
|
256
|
+
cxa({ foo: true }, { bar: false }, null, { "--foobar": "hello" });
|
|
246
257
|
//=> "foo --foobar"
|
|
247
258
|
|
|
248
259
|
// Arrays
|
|
@@ -254,7 +265,7 @@ cxa(["foo"], ["", 0, false, "bar"], [["baz", [["hello"], "there"]]]);
|
|
|
254
265
|
//=> "foo bar baz hello there"
|
|
255
266
|
|
|
256
267
|
// Kitchen sink (with nesting)
|
|
257
|
-
cxa("foo", [1 && "bar", { baz:false, bat:null }, ["hello", ["world"]]], "cya");
|
|
268
|
+
cxa("foo", [1 && "bar", { baz: false, bat: null }, ["hello", ["world"]]], "cya");
|
|
258
269
|
//=> "foo bar hello world cya"
|
|
259
270
|
```
|
|
260
271
|
|
|
@@ -285,11 +296,20 @@ const a = { b: 2, a: 1, nested: { y: 2, x: 1 } };
|
|
|
285
296
|
const b = { nested: { x: 1, y: 2 }, a: 1, b: 2 };
|
|
286
297
|
console.log(deepEqualIgnoringKeyOrder(a, b)); // true
|
|
287
298
|
|
|
288
|
-
const arr1 = [
|
|
289
|
-
|
|
299
|
+
const arr1 = [
|
|
300
|
+
{ a: 1, b: 2 },
|
|
301
|
+
{ c: 3, d: 4 },
|
|
302
|
+
];
|
|
303
|
+
const arr2 = [
|
|
304
|
+
{ b: 2, a: 1 },
|
|
305
|
+
{ d: 4, c: 3 },
|
|
306
|
+
];
|
|
290
307
|
console.log(deepEqualIgnoringKeyOrder(arr1, arr2)); // true (objects equal, same array order)
|
|
291
308
|
|
|
292
|
-
const arr3 = [
|
|
309
|
+
const arr3 = [
|
|
310
|
+
{ d: 4, c: 3 },
|
|
311
|
+
{ b: 2, a: 1 },
|
|
312
|
+
];
|
|
293
313
|
console.log(deepEqualIgnoringKeyOrder(arr1, arr3)); // false (array order differs)
|
|
294
314
|
```
|
|
295
315
|
|
|
@@ -302,7 +322,7 @@ import { downloadFile } from "@uxf/core/utils/download-file";
|
|
|
302
322
|
import { FormEventHandler } from "react";
|
|
303
323
|
|
|
304
324
|
const submitHandler: FormEventHandler<HTMLFormElement> = () => {
|
|
305
|
-
downloadFile("https://example.com/file", "file.txt")
|
|
325
|
+
downloadFile("https://example.com/file", "file.txt");
|
|
306
326
|
};
|
|
307
327
|
```
|
|
308
328
|
|
|
@@ -313,7 +333,7 @@ Escapes all double quotes (`"`) in a string by replacing them with `\"`.
|
|
|
313
333
|
```ts
|
|
314
334
|
import { escapeQuotes } from "@uxf/core/utils/escape-quotes";
|
|
315
335
|
|
|
316
|
-
escapeQuotes('The "quick" fox');
|
|
336
|
+
escapeQuotes('The "quick" fox');
|
|
317
337
|
// Output: The \"quick\" fox
|
|
318
338
|
```
|
|
319
339
|
|
|
@@ -324,9 +344,9 @@ Converts a 0-based index to a 1-based (human-readable) index.
|
|
|
324
344
|
```tsx
|
|
325
345
|
import { humanIndex } from "@uxf/core/utils/human-index";
|
|
326
346
|
|
|
327
|
-
humanIndex(0);
|
|
328
|
-
humanIndex(9);
|
|
329
|
-
humanIndex(-1);
|
|
347
|
+
humanIndex(0); /* returns 1 */
|
|
348
|
+
humanIndex(9); /* returns 10 */
|
|
349
|
+
humanIndex(-1); /* throws error */
|
|
330
350
|
```
|
|
331
351
|
|
|
332
352
|
## filterNullish
|
|
@@ -369,7 +389,7 @@ const props = {
|
|
|
369
389
|
const htmlAttrs = filterAriaAndDataAttrs(props);
|
|
370
390
|
// Result: { "aria-label": "Close button", "data-testid": "close-btn" }
|
|
371
391
|
|
|
372
|
-
<button {...htmlAttrs}>Close</button
|
|
392
|
+
<button {...htmlAttrs}>Close</button>;
|
|
373
393
|
```
|
|
374
394
|
|
|
375
395
|
```tsx
|
|
@@ -380,16 +400,13 @@ function CustomInput({ label, onChange, ...restProps }) {
|
|
|
380
400
|
return <input {...accessibilityAttrs} onChange={onChange} />;
|
|
381
401
|
}
|
|
382
402
|
|
|
383
|
-
<CustomInput
|
|
384
|
-
aria-describedby="helper-text"
|
|
385
|
-
data-analytics="email-input"
|
|
386
|
-
customProp="ignored"
|
|
387
|
-
/>
|
|
403
|
+
<CustomInput aria-describedby="helper-text" data-analytics="email-input" customProp="ignored" />;
|
|
388
404
|
```
|
|
389
405
|
|
|
390
406
|
### formatBytes
|
|
391
407
|
|
|
392
408
|
Appends suitable unit to the byte value of data size.
|
|
409
|
+
|
|
393
410
|
```ts
|
|
394
411
|
formatBytes(17.5 * 1024);
|
|
395
412
|
//=> "17.5 kB"
|
|
@@ -423,7 +440,7 @@ Re-export of [lodash.isequal](https://lodash.com/docs/#isEqual). Performs a deep
|
|
|
423
440
|
import { isEqual } from "@uxf/core/utils/is-equal";
|
|
424
441
|
|
|
425
442
|
isEqual({ a: 1, b: [2, 3] }, { a: 1, b: [2, 3] }); /* returns true */
|
|
426
|
-
isEqual({ a: 1 }, { a: 2 });
|
|
443
|
+
isEqual({ a: 1 }, { a: 2 }); /* returns false */
|
|
427
444
|
```
|
|
428
445
|
|
|
429
446
|
## isEmpty
|
|
@@ -444,11 +461,11 @@ Checks if a number is even.
|
|
|
444
461
|
```tsx
|
|
445
462
|
import { isEven } from "@uxf/core/utils/is-even";
|
|
446
463
|
|
|
447
|
-
isEven(2);
|
|
448
|
-
isEven(4);
|
|
449
|
-
isEven(1);
|
|
450
|
-
isEven(3);
|
|
451
|
-
isEven(0);
|
|
464
|
+
isEven(2); /* returns true */
|
|
465
|
+
isEven(4); /* returns true */
|
|
466
|
+
isEven(1); /* returns false */
|
|
467
|
+
isEven(3); /* returns false */
|
|
468
|
+
isEven(0); /* returns true */
|
|
452
469
|
isEven(-2); /* returns true */
|
|
453
470
|
```
|
|
454
471
|
|
|
@@ -459,11 +476,11 @@ Checks if a number is odd.
|
|
|
459
476
|
```tsx
|
|
460
477
|
import { isOdd } from "@uxf/core/utils/is-odd";
|
|
461
478
|
|
|
462
|
-
isOdd(1);
|
|
463
|
-
isOdd(3);
|
|
464
|
-
isOdd(2);
|
|
465
|
-
isOdd(4);
|
|
466
|
-
isOdd(0);
|
|
479
|
+
isOdd(1); /* returns true */
|
|
480
|
+
isOdd(3); /* returns true */
|
|
481
|
+
isOdd(2); /* returns false */
|
|
482
|
+
isOdd(4); /* returns false */
|
|
483
|
+
isOdd(0); /* returns false */
|
|
467
484
|
isOdd(-1); /* returns true */
|
|
468
485
|
```
|
|
469
486
|
|
|
@@ -482,13 +499,13 @@ const serverExample = isServer; /* returns true if DOM is NOT available */
|
|
|
482
499
|
```tsx
|
|
483
500
|
import { isNil } from "@uxf/core/utils/is-nil";
|
|
484
501
|
|
|
485
|
-
isNil(null);
|
|
502
|
+
isNil(null); /* returns true */
|
|
486
503
|
isNil(undefined); /* returns true */
|
|
487
|
-
isNil(true);
|
|
488
|
-
isNil(1);
|
|
489
|
-
isNil(0);
|
|
490
|
-
isNil([]);
|
|
491
|
-
isNil("string");
|
|
504
|
+
isNil(true); /* returns false */
|
|
505
|
+
isNil(1); /* returns false */
|
|
506
|
+
isNil(0); /* returns false */
|
|
507
|
+
isNil([]); /* returns false */
|
|
508
|
+
isNil("string"); /* returns false */
|
|
492
509
|
```
|
|
493
510
|
|
|
494
511
|
## isNotNil
|
|
@@ -496,13 +513,13 @@ isNil("string"); /* returns false */
|
|
|
496
513
|
```tsx
|
|
497
514
|
import { isNotNil } from "@uxf/core/utils/is-not-nil";
|
|
498
515
|
|
|
499
|
-
isNotNil(null);
|
|
516
|
+
isNotNil(null); /* returns false */
|
|
500
517
|
isNotNil(undefined); /* returns false */
|
|
501
|
-
isNotNil(true);
|
|
502
|
-
isNotNil(1);
|
|
503
|
-
isNotNil(0);
|
|
504
|
-
isNotNil([]);
|
|
505
|
-
isNotNil("string");
|
|
518
|
+
isNotNil(true); /* returns true */
|
|
519
|
+
isNotNil(1); /* returns true */
|
|
520
|
+
isNotNil(0); /* returns true */
|
|
521
|
+
isNotNil([]); /* returns true */
|
|
522
|
+
isNotNil("string"); /* returns true */
|
|
506
523
|
```
|
|
507
524
|
|
|
508
525
|
## isNotNilNorEmpty
|
|
@@ -529,18 +546,18 @@ Type guard that checks if a value is a plain object. Returns `false` for arrays,
|
|
|
529
546
|
```tsx
|
|
530
547
|
import { isPlainObject } from "@uxf/core/utils/is-plain-object";
|
|
531
548
|
|
|
532
|
-
isPlainObject({});
|
|
533
|
-
isPlainObject({ a: 1 });
|
|
534
|
-
isPlainObject([]);
|
|
535
|
-
isPlainObject([1, 2, 3]);
|
|
536
|
-
isPlainObject(new Date());
|
|
537
|
-
isPlainObject(new Map());
|
|
538
|
-
isPlainObject(new Set());
|
|
539
|
-
isPlainObject(/regex/);
|
|
540
|
-
isPlainObject(null);
|
|
541
|
-
isPlainObject(undefined);
|
|
542
|
-
isPlainObject("string");
|
|
543
|
-
isPlainObject(123);
|
|
549
|
+
isPlainObject({}); /* returns true */
|
|
550
|
+
isPlainObject({ a: 1 }); /* returns true */
|
|
551
|
+
isPlainObject([]); /* returns false */
|
|
552
|
+
isPlainObject([1, 2, 3]); /* returns false */
|
|
553
|
+
isPlainObject(new Date()); /* returns false */
|
|
554
|
+
isPlainObject(new Map()); /* returns false */
|
|
555
|
+
isPlainObject(new Set()); /* returns false */
|
|
556
|
+
isPlainObject(/regex/); /* returns false */
|
|
557
|
+
isPlainObject(null); /* returns false */
|
|
558
|
+
isPlainObject(undefined); /* returns false */
|
|
559
|
+
isPlainObject("string"); /* returns false */
|
|
560
|
+
isPlainObject(123); /* returns false */
|
|
544
561
|
```
|
|
545
562
|
|
|
546
563
|
## last
|
|
@@ -549,7 +566,7 @@ isPlainObject(123); /* returns false */
|
|
|
549
566
|
import { last } from "@uxf/core/utils/last";
|
|
550
567
|
|
|
551
568
|
last([1, 2]); /* returns 2 */
|
|
552
|
-
last([]);
|
|
569
|
+
last([]); /* returns undefined */
|
|
553
570
|
```
|
|
554
571
|
|
|
555
572
|
## nonEmptyArrayOrNull
|
|
@@ -559,11 +576,11 @@ Converts empty arrays, `null`, or `undefined` values to `null`, leaving all non-
|
|
|
559
576
|
```tsx
|
|
560
577
|
import { nonEmptyArrayOrNull } from "@uxf/core/utils/non-empty-array-or-null";
|
|
561
578
|
|
|
562
|
-
nonEmptyArrayOrNull([]);
|
|
563
|
-
nonEmptyArrayOrNull(null);
|
|
564
|
-
nonEmptyArrayOrNull(undefined);
|
|
565
|
-
nonEmptyArrayOrNull([1, 2, 3]);
|
|
566
|
-
nonEmptyArrayOrNull(["a", "b"]);
|
|
579
|
+
nonEmptyArrayOrNull([]); /* returns null */
|
|
580
|
+
nonEmptyArrayOrNull(null); /* returns null */
|
|
581
|
+
nonEmptyArrayOrNull(undefined); /* returns null */
|
|
582
|
+
nonEmptyArrayOrNull([1, 2, 3]); /* returns [1, 2, 3] */
|
|
583
|
+
nonEmptyArrayOrNull(["a", "b"]); /* returns ["a", "b"] */
|
|
567
584
|
```
|
|
568
585
|
|
|
569
586
|
## nonEmptyStringOrNull
|
|
@@ -573,11 +590,11 @@ Converts empty strings and `undefined` values to `null`, leaving all other strin
|
|
|
573
590
|
```tsx
|
|
574
591
|
import { nonEmptyStringOrNull } from "@uxf/core/utils/non-empty-string-or-null";
|
|
575
592
|
|
|
576
|
-
nonEmptyStringOrNull("");
|
|
577
|
-
nonEmptyStringOrNull(undefined);
|
|
578
|
-
nonEmptyStringOrNull(null);
|
|
579
|
-
nonEmptyStringOrNull("test");
|
|
580
|
-
nonEmptyStringOrNull(" ");
|
|
593
|
+
nonEmptyStringOrNull(""); /* returns null */
|
|
594
|
+
nonEmptyStringOrNull(undefined); /* returns null */
|
|
595
|
+
nonEmptyStringOrNull(null); /* returns null */
|
|
596
|
+
nonEmptyStringOrNull("test"); /* returns "test" */
|
|
597
|
+
nonEmptyStringOrNull(" "); /* returns " " - non-empty string */
|
|
581
598
|
```
|
|
582
599
|
|
|
583
600
|
## nullishToEmptyString
|
|
@@ -587,10 +604,10 @@ Converts `null` or `undefined` values to an empty string, leaving all other stri
|
|
|
587
604
|
```tsx
|
|
588
605
|
import { nullishToEmptyString } from "@uxf/core/utils/nullish-to-empty-string";
|
|
589
606
|
|
|
590
|
-
nullishToEmptyString(null);
|
|
591
|
-
nullishToEmptyString(undefined);
|
|
592
|
-
nullishToEmptyString("");
|
|
593
|
-
nullishToEmptyString("hello world");
|
|
607
|
+
nullishToEmptyString(null); /* returns "" */
|
|
608
|
+
nullishToEmptyString(undefined); /* returns "" */
|
|
609
|
+
nullishToEmptyString(""); /* returns "" */
|
|
610
|
+
nullishToEmptyString("hello world"); /* returns "hello world" */
|
|
594
611
|
```
|
|
595
612
|
|
|
596
613
|
## numberOrNull
|
|
@@ -600,13 +617,13 @@ Converts `NaN`, `null`, or `undefined` values to `null`, leaving all valid numbe
|
|
|
600
617
|
```tsx
|
|
601
618
|
import { numberOrNull } from "@uxf/core/utils/number-or-null";
|
|
602
619
|
|
|
603
|
-
numberOrNull(0);
|
|
604
|
-
numberOrNull(42);
|
|
605
|
-
numberOrNull(-1);
|
|
606
|
-
numberOrNull(3.14);
|
|
607
|
-
numberOrNull(NaN);
|
|
608
|
-
numberOrNull(null);
|
|
609
|
-
numberOrNull(undefined);
|
|
620
|
+
numberOrNull(0); /* returns 0 */
|
|
621
|
+
numberOrNull(42); /* returns 42 */
|
|
622
|
+
numberOrNull(-1); /* returns -1 */
|
|
623
|
+
numberOrNull(3.14); /* returns 3.14 */
|
|
624
|
+
numberOrNull(NaN); /* returns null */
|
|
625
|
+
numberOrNull(null); /* returns null */
|
|
626
|
+
numberOrNull(undefined); /* returns null */
|
|
610
627
|
```
|
|
611
628
|
|
|
612
629
|
## plural
|
|
@@ -639,7 +656,26 @@ Re-export of [qs](https://github.com/ljharb/qs). A querystring parsing and strin
|
|
|
639
656
|
import { stringify, parse } from "@uxf/core/utils/qs";
|
|
640
657
|
|
|
641
658
|
stringify({ a: "b", c: [1, 2] }); /* returns "a=b&c%5B0%5D=1&c%5B1%5D=2" */
|
|
642
|
-
parse("a=b&c=1");
|
|
659
|
+
parse("a=b&c=1"); /* returns { a: "b", c: "1" } */
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
## safeLocalStorage / safeSessionStorage
|
|
663
|
+
|
|
664
|
+
Web storage that never throws. Safari in private mode, iOS with cookies blocked, sandboxed iframes and several in-app browsers throw `SecurityError: The operation is insecure.` on **reads as well as writes** — and in some browsers even on the bare `window.localStorage` property access — so unguarded storage access during render can take the whole app down. A full quota throws `QuotaExceededError` the same way.
|
|
665
|
+
|
|
666
|
+
A read that cannot happen returns `null`, a write that cannot happen returns `false`. The storage object is resolved per call, so importing this module on the server is safe.
|
|
667
|
+
|
|
668
|
+
```tsx
|
|
669
|
+
import { safeLocalStorage, safeSessionStorage } from "@uxf/core/utils/safe-storage";
|
|
670
|
+
|
|
671
|
+
safeLocalStorage.setItem("theme", "dark"); /* returns true, or false when storage is unavailable */
|
|
672
|
+
safeLocalStorage.getItem("theme"); /* returns "dark", or null when storage is unavailable */
|
|
673
|
+
safeLocalStorage.removeItem("theme"); /* returns true when it went through */
|
|
674
|
+
safeLocalStorage.clear(); /* returns true when it went through */
|
|
675
|
+
safeLocalStorage.length; /* returns 0 when storage is unavailable */
|
|
676
|
+
safeLocalStorage.key(0); /* returns null when storage is unavailable */
|
|
677
|
+
|
|
678
|
+
safeSessionStorage.getItem("wizard-step"); /* same API for sessionStorage */
|
|
643
679
|
```
|
|
644
680
|
|
|
645
681
|
## slugify
|
|
@@ -685,10 +721,10 @@ const example = trimTrailingZeros("120,450"); /* returns "120,45" */
|
|
|
685
721
|
```
|
|
686
722
|
|
|
687
723
|
## Validators
|
|
724
|
+
|
|
688
725
|
```tsx
|
|
689
726
|
import { Validator } from "@uxf/core";
|
|
690
727
|
|
|
691
728
|
Validator.isEmail("...");
|
|
692
729
|
Validator.isPhone("...");
|
|
693
730
|
```
|
|
694
|
-
|
package/package.json
CHANGED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface SafeStorage {
|
|
2
|
+
clear: () => boolean;
|
|
3
|
+
getItem: (key: string) => string | null;
|
|
4
|
+
key: (index: number) => string | null;
|
|
5
|
+
readonly length: number;
|
|
6
|
+
removeItem: (key: string) => boolean;
|
|
7
|
+
setItem: (key: string, value: string) => boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare const safeLocalStorage: SafeStorage;
|
|
10
|
+
export declare const safeSessionStorage: SafeStorage;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.safeSessionStorage = exports.safeLocalStorage = void 0;
|
|
4
|
+
const is_browser_1 = require("./is-browser");
|
|
5
|
+
const is_nil_1 = require("./is-nil");
|
|
6
|
+
/**
|
|
7
|
+
* Web storage is not always usable. Safari in private mode, iOS with cookies blocked, sandboxed
|
|
8
|
+
* iframes and several in-app browsers throw `SecurityError: The operation is insecure.` — and a full
|
|
9
|
+
* quota throws `QuotaExceededError` — on **reads as well as writes**, and in some browsers even on
|
|
10
|
+
* the bare `window.localStorage` property access. An unguarded read during render therefore takes
|
|
11
|
+
* the whole app down.
|
|
12
|
+
*
|
|
13
|
+
* These wrappers never throw: a read that cannot happen returns `null`, a write that cannot happen
|
|
14
|
+
* returns `false`. The storage object is resolved per call, so a page that gains (or loses) access
|
|
15
|
+
* mid-session is handled too, and importing this module on the server is safe.
|
|
16
|
+
*/
|
|
17
|
+
function getWebStorage(name) {
|
|
18
|
+
if (!is_browser_1.isBrowser) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
return window[name];
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function createSafeStorage(name) {
|
|
29
|
+
return {
|
|
30
|
+
clear() {
|
|
31
|
+
const storage = getWebStorage(name);
|
|
32
|
+
if ((0, is_nil_1.isNil)(storage)) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
storage.clear();
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
getItem(key) {
|
|
44
|
+
const storage = getWebStorage(name);
|
|
45
|
+
if ((0, is_nil_1.isNil)(storage)) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
return storage.getItem(key);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
key(index) {
|
|
56
|
+
const storage = getWebStorage(name);
|
|
57
|
+
if ((0, is_nil_1.isNil)(storage)) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
return storage.key(index);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
get length() {
|
|
68
|
+
const storage = getWebStorage(name);
|
|
69
|
+
if ((0, is_nil_1.isNil)(storage)) {
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
return storage.length;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return 0;
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
removeItem(key) {
|
|
80
|
+
const storage = getWebStorage(name);
|
|
81
|
+
if ((0, is_nil_1.isNil)(storage)) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
storage.removeItem(key);
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
setItem(key, value) {
|
|
93
|
+
const storage = getWebStorage(name);
|
|
94
|
+
if ((0, is_nil_1.isNil)(storage)) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
storage.setItem(key, value);
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
exports.safeLocalStorage = createSafeStorage("localStorage");
|
|
108
|
+
exports.safeSessionStorage = createSafeStorage("sessionStorage");
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const safe_storage_1 = require("./safe-storage");
|
|
4
|
+
function throwSecurityError() {
|
|
5
|
+
throw new DOMException("The operation is insecure.", "SecurityError");
|
|
6
|
+
}
|
|
7
|
+
describe.each([
|
|
8
|
+
["safeLocalStorage", safe_storage_1.safeLocalStorage, window.localStorage],
|
|
9
|
+
["safeSessionStorage", safe_storage_1.safeSessionStorage, window.sessionStorage],
|
|
10
|
+
])("%s", (_name, safeStorage, storage) => {
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
jest.restoreAllMocks();
|
|
13
|
+
storage.clear();
|
|
14
|
+
});
|
|
15
|
+
describe("with a working storage", () => {
|
|
16
|
+
it("reads and writes", () => {
|
|
17
|
+
expect(safeStorage.getItem("theme")).toBeNull();
|
|
18
|
+
expect(safeStorage.setItem("theme", "dark")).toBe(true);
|
|
19
|
+
expect(safeStorage.getItem("theme")).toBe("dark");
|
|
20
|
+
});
|
|
21
|
+
it("removes and clears", () => {
|
|
22
|
+
safeStorage.setItem("theme", "dark");
|
|
23
|
+
expect(safeStorage.removeItem("theme")).toBe(true);
|
|
24
|
+
expect(safeStorage.getItem("theme")).toBeNull();
|
|
25
|
+
safeStorage.setItem("theme", "dark");
|
|
26
|
+
expect(safeStorage.clear()).toBe(true);
|
|
27
|
+
expect(safeStorage.length).toBe(0);
|
|
28
|
+
});
|
|
29
|
+
it("enumerates keys", () => {
|
|
30
|
+
safeStorage.setItem("theme", "dark");
|
|
31
|
+
expect(safeStorage.length).toBe(1);
|
|
32
|
+
expect(safeStorage.key(0)).toBe("theme");
|
|
33
|
+
expect(safeStorage.key(1)).toBeNull();
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
describe("with a storage that throws (private mode, blocked storage, full quota)", () => {
|
|
37
|
+
it("returns null instead of throwing on a read", () => {
|
|
38
|
+
jest.spyOn(Storage.prototype, "getItem").mockImplementation(throwSecurityError);
|
|
39
|
+
expect(() => safeStorage.getItem("theme")).not.toThrow();
|
|
40
|
+
expect(safeStorage.getItem("theme")).toBeNull();
|
|
41
|
+
});
|
|
42
|
+
it("returns false instead of throwing on a write", () => {
|
|
43
|
+
jest.spyOn(Storage.prototype, "setItem").mockImplementation(throwSecurityError);
|
|
44
|
+
jest.spyOn(Storage.prototype, "removeItem").mockImplementation(throwSecurityError);
|
|
45
|
+
jest.spyOn(Storage.prototype, "clear").mockImplementation(throwSecurityError);
|
|
46
|
+
expect(safeStorage.setItem("theme", "dark")).toBe(false);
|
|
47
|
+
expect(safeStorage.removeItem("theme")).toBe(false);
|
|
48
|
+
expect(safeStorage.clear()).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
it("returns empty values instead of throwing on enumeration", () => {
|
|
51
|
+
jest.spyOn(Storage.prototype, "key").mockImplementation(throwSecurityError);
|
|
52
|
+
jest.spyOn(Storage.prototype, "length", "get").mockImplementation(throwSecurityError);
|
|
53
|
+
expect(safeStorage.key(0)).toBeNull();
|
|
54
|
+
expect(safeStorage.length).toBe(0);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
});
|