@uxf/core 11.124.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 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
- - `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`
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
- const textarea = textareaRef.current;
115
-
116
- if (isNotNil(textarea)) {
117
- return;
118
- }
119
-
120
- const cleanup = adjustTextareaHeight(textarea);
121
-
122
- return () => cleanup();
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 *any* number of arguments, each of which can be an Object, Array, Boolean, or String.
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 = [{ a: 1, b: 2 }, { c: 3, d: 4 }];
289
- const arr2 = [{ b: 2, a: 1 }, { d: 4, c: 3 }];
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 = [{ d: 4, c: 3 }, { b: 2, a: 1 }];
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); /* returns 1 */
328
- humanIndex(9); /* returns 10 */
329
- humanIndex(-1); /* throws error */
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 }); /* returns false */
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); /* returns true */
448
- isEven(4); /* returns true */
449
- isEven(1); /* returns false */
450
- isEven(3); /* returns false */
451
- isEven(0); /* returns true */
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); /* returns true */
463
- isOdd(3); /* returns true */
464
- isOdd(2); /* returns false */
465
- isOdd(4); /* returns false */
466
- isOdd(0); /* returns false */
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); /* returns true */
502
+ isNil(null); /* returns true */
486
503
  isNil(undefined); /* returns true */
487
- isNil(true); /* returns false */
488
- isNil(1); /* returns false */
489
- isNil(0); /* returns false */
490
- isNil([]); /* returns false */
491
- isNil("string"); /* returns false */
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); /* returns false */
516
+ isNotNil(null); /* returns false */
500
517
  isNotNil(undefined); /* returns false */
501
- isNotNil(true); /* returns true */
502
- isNotNil(1); /* returns true */
503
- isNotNil(0); /* returns true */
504
- isNotNil([]); /* returns true */
505
- isNotNil("string"); /* returns true */
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({}); /* returns true */
533
- isPlainObject({ a: 1 }); /* returns true */
534
- isPlainObject([]); /* returns false */
535
- isPlainObject([1, 2, 3]); /* returns false */
536
- isPlainObject(new Date()); /* returns false */
537
- isPlainObject(new Map()); /* returns false */
538
- isPlainObject(new Set()); /* returns false */
539
- isPlainObject(/regex/); /* returns false */
540
- isPlainObject(null); /* returns false */
541
- isPlainObject(undefined); /* returns false */
542
- isPlainObject("string"); /* returns false */
543
- isPlainObject(123); /* returns false */
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([]); /* returns undefined */
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([]); /* returns null */
563
- nonEmptyArrayOrNull(null); /* returns null */
564
- nonEmptyArrayOrNull(undefined); /* returns null */
565
- nonEmptyArrayOrNull([1, 2, 3]); /* returns [1, 2, 3] */
566
- nonEmptyArrayOrNull(["a", "b"]); /* returns ["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(""); /* returns null */
577
- nonEmptyStringOrNull(undefined); /* returns null */
578
- nonEmptyStringOrNull(null); /* returns null */
579
- nonEmptyStringOrNull("test"); /* returns "test" */
580
- nonEmptyStringOrNull(" "); /* returns " " - non-empty string */
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); /* returns "" */
591
- nullishToEmptyString(undefined); /* returns "" */
592
- nullishToEmptyString(""); /* returns "" */
593
- nullishToEmptyString("hello world"); /* returns "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); /* returns 0 */
604
- numberOrNull(42); /* returns 42 */
605
- numberOrNull(-1); /* returns -1 */
606
- numberOrNull(3.14); /* returns 3.14 */
607
- numberOrNull(NaN); /* returns null */
608
- numberOrNull(null); /* returns null */
609
- numberOrNull(undefined); /* returns null */
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"); /* returns { 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uxf/core",
3
- "version": "11.124.0",
3
+ "version": "11.126.0",
4
4
  "description": "UXF Core",
5
5
  "author": "Petr Vejvoda <vejvoda@uxf.cz>",
6
6
  "homepage": "https://gitlab.com/uxf-npm/core#readme",
@@ -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
+ });