@lipemat/js-helpers 1.0.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.
Files changed (115) hide show
  1. package/README.md +61 -0
  2. package/dist/classes/classes.d.ts +28 -0
  3. package/dist/classes/classes.d.ts.map +1 -0
  4. package/dist/classes/classes.js +50 -0
  5. package/dist/classes/classes.js.map +1 -0
  6. package/dist/colors/colors.d.ts +14 -0
  7. package/dist/colors/colors.d.ts.map +1 -0
  8. package/dist/colors/colors.js +63 -0
  9. package/dist/colors/colors.js.map +1 -0
  10. package/dist/csvExport/csvExport.d.ts +19 -0
  11. package/dist/csvExport/csvExport.d.ts.map +1 -0
  12. package/dist/csvExport/csvExport.js +74 -0
  13. package/dist/csvExport/csvExport.js.map +1 -0
  14. package/dist/date/date.d.ts +10 -0
  15. package/dist/date/date.d.ts.map +1 -0
  16. package/dist/date/date.js +20 -0
  17. package/dist/date/date.js.map +1 -0
  18. package/dist/debounce/debounce.d.ts +17 -0
  19. package/dist/debounce/debounce.d.ts.map +1 -0
  20. package/dist/debounce/debounce.js +34 -0
  21. package/dist/debounce/debounce.js.map +1 -0
  22. package/dist/delay/delay.d.ts +18 -0
  23. package/dist/delay/delay.d.ts.map +1 -0
  24. package/dist/delay/delay.js +20 -0
  25. package/dist/delay/delay.js.map +1 -0
  26. package/dist/device/device.d.ts +26 -0
  27. package/dist/device/device.d.ts.map +1 -0
  28. package/dist/device/device.js +42 -0
  29. package/dist/device/device.js.map +1 -0
  30. package/dist/dom-ready/dom-ready.d.ts +12 -0
  31. package/dist/dom-ready/dom-ready.d.ts.map +1 -0
  32. package/dist/dom-ready/dom-ready.js +19 -0
  33. package/dist/dom-ready/dom-ready.js.map +1 -0
  34. package/dist/error/error.d.ts +28 -0
  35. package/dist/error/error.d.ts.map +1 -0
  36. package/dist/error/error.js +33 -0
  37. package/dist/error/error.js.map +1 -0
  38. package/dist/escaping/escaping.d.ts +17 -0
  39. package/dist/escaping/escaping.d.ts.map +1 -0
  40. package/dist/escaping/escaping.js +39 -0
  41. package/dist/escaping/escaping.js.map +1 -0
  42. package/dist/index.d.ts +25 -0
  43. package/dist/index.d.ts.map +1 -0
  44. package/dist/index.js +25 -0
  45. package/dist/index.js.map +1 -0
  46. package/dist/injectScript/injectScript.d.ts +14 -0
  47. package/dist/injectScript/injectScript.d.ts.map +1 -0
  48. package/dist/injectScript/injectScript.js +30 -0
  49. package/dist/injectScript/injectScript.js.map +1 -0
  50. package/dist/memoize/memoize.d.ts +13 -0
  51. package/dist/memoize/memoize.d.ts.map +1 -0
  52. package/dist/memoize/memoize.js +19 -0
  53. package/dist/memoize/memoize.js.map +1 -0
  54. package/dist/noop/noop.d.ts +9 -0
  55. package/dist/noop/noop.d.ts.map +1 -0
  56. package/dist/noop/noop.js +10 -0
  57. package/dist/noop/noop.js.map +1 -0
  58. package/dist/objects/objects.d.ts +20 -0
  59. package/dist/objects/objects.d.ts.map +1 -0
  60. package/dist/objects/objects.js +24 -0
  61. package/dist/objects/objects.js.map +1 -0
  62. package/dist/once/once.d.ts +9 -0
  63. package/dist/once/once.d.ts.map +1 -0
  64. package/dist/once/once.js +17 -0
  65. package/dist/once/once.js.map +1 -0
  66. package/dist/string/string.d.ts +34 -0
  67. package/dist/string/string.d.ts.map +1 -0
  68. package/dist/string/string.js +65 -0
  69. package/dist/string/string.js.map +1 -0
  70. package/dist/throttle/throttle.d.ts +17 -0
  71. package/dist/throttle/throttle.d.ts.map +1 -0
  72. package/dist/throttle/throttle.js +43 -0
  73. package/dist/throttle/throttle.js.map +1 -0
  74. package/dist/url/url.d.ts +17 -0
  75. package/dist/url/url.d.ts.map +1 -0
  76. package/dist/url/url.js +28 -0
  77. package/dist/url/url.js.map +1 -0
  78. package/package.json +71 -0
  79. package/src/classes/Readme.md +28 -0
  80. package/src/classes/classes.ts +51 -0
  81. package/src/colors/Readme.md +28 -0
  82. package/src/colors/colors.ts +80 -0
  83. package/src/csvExport/Readme.md +35 -0
  84. package/src/csvExport/csvExport.ts +89 -0
  85. package/src/date/Readme.md +21 -0
  86. package/src/date/date.ts +23 -0
  87. package/src/debounce/Readme.md +27 -0
  88. package/src/debounce/debounce.ts +47 -0
  89. package/src/delay/Readme.md +20 -0
  90. package/src/delay/delay.ts +19 -0
  91. package/src/device/Readme.md +31 -0
  92. package/src/device/device.ts +46 -0
  93. package/src/dom-ready/Readme.md +27 -0
  94. package/src/dom-ready/dom-ready.ts +22 -0
  95. package/src/error/Readme.md +40 -0
  96. package/src/error/error.ts +49 -0
  97. package/src/escaping/Readme.md +23 -0
  98. package/src/escaping/escaping.ts +41 -0
  99. package/src/index.ts +24 -0
  100. package/src/injectScript/Readme.md +26 -0
  101. package/src/injectScript/injectScript.ts +31 -0
  102. package/src/memoize/Readme.md +28 -0
  103. package/src/memoize/memoize.ts +27 -0
  104. package/src/noop/Readme.md +19 -0
  105. package/src/noop/noop.ts +9 -0
  106. package/src/objects/Readme.md +31 -0
  107. package/src/objects/objects.ts +26 -0
  108. package/src/once/Readme.md +26 -0
  109. package/src/once/once.ts +22 -0
  110. package/src/string/Readme.md +37 -0
  111. package/src/string/string.ts +72 -0
  112. package/src/throttle/Readme.md +27 -0
  113. package/src/throttle/throttle.ts +57 -0
  114. package/src/url/Readme.md +32 -0
  115. package/src/url/url.ts +35 -0
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@lipemat/js-helpers",
3
+ "version": "1.0.0",
4
+ "description": "Framework-agnostic TypeScript helper utilities used across @lipemat projects",
5
+ "author": "Mat Lipe",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "sideEffects": false,
9
+ "engines": {
10
+ "node": ">=22.21.1"
11
+ },
12
+ "main": "dist/index.js",
13
+ "module": "dist/index.js",
14
+ "types": "dist/index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "default": "./dist/index.js"
19
+ }
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "src"
24
+ ],
25
+ "keywords": [
26
+ "helpers",
27
+ "utilities",
28
+ "typescript",
29
+ "debounce",
30
+ "throttle",
31
+ "memoize"
32
+ ],
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/lipemat/js-helpers.git"
36
+ },
37
+ "bugs": {
38
+ "url": "https://github.com/lipemat/js-helpers/issues"
39
+ },
40
+ "homepage": "https://github.com/lipemat/js-helpers#readme",
41
+ "scripts": {
42
+ "build": "tsc",
43
+ "lint": "eslint ./src --fix --cache",
44
+ "postinstall": "test -f ./dev/bin/npm-post-install && bash ./dev/bin/npm-post-install || true",
45
+ "prepublishOnly": "rm -rf dist/* && yarn run build",
46
+ "test": "jest",
47
+ "validate-ts": "tsc --noEmit",
48
+ "watch": "tsc --watch"
49
+ },
50
+ "dependencies": {
51
+ "dompurify": "^3.4.0"
52
+ },
53
+ "devDependencies": {
54
+ "@babel/core": "^7.28.5",
55
+ "@babel/preset-env": "^7.28.5",
56
+ "@babel/preset-typescript": "^7.28.5",
57
+ "@lipemat/eslint-config": "^5.2.0",
58
+ "@testing-library/dom": "^10.4.0",
59
+ "@types/jest": "^30.0.0",
60
+ "@types/node": "^22.19.3",
61
+ "babel-jest": "^30.2.0",
62
+ "eslint": "^9",
63
+ "jest": "^30.2.0",
64
+ "jest-environment-jsdom": "^30.2.0",
65
+ "typescript": "^5.9.3"
66
+ },
67
+ "peerDependencies": {
68
+ "typescript": "^5.9.3"
69
+ },
70
+ "packageManager": "yarn@4.17.0"
71
+ }
@@ -0,0 +1,28 @@
1
+ # classes
2
+
3
+ Utilities for manipulating CSS class name strings.
4
+
5
+ ## Exports
6
+
7
+ ### `classNameAmend( existing, className, add ): string`
8
+
9
+ Add or remove a single class name from a space-separated string of classes. Duplicates are removed.
10
+
11
+ - `existing: string | undefined` — current class string.
12
+ - `className: string` — class to add or remove.
13
+ - `add: boolean` — `true` to add, `false` to remove.
14
+
15
+ ### `sanitizeCssModuleClass( className ): string`
16
+
17
+ Normalize a CSS class name produced by a PHP CSS module: trims whitespace, converts inner spaces to dots, and ensures a leading dot. Returns `''` for `null`.
18
+
19
+ ## Usage
20
+
21
+ ```ts
22
+ import {classNameAmend, sanitizeCssModuleClass} from '@lipemat/js-helpers';
23
+
24
+ classNameAmend( 'a b', 'c', true ); // 'a b c'
25
+ classNameAmend( 'a b c', 'b', false ); // 'a c'
26
+
27
+ sanitizeCssModuleClass( 'card active' ); // '.card.active'
28
+ ```
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Utility functions for manipulating CSS class names.
3
+ *
4
+ * @version 1.4.0
5
+ */
6
+
7
+
8
+ /**
9
+ * Add or remove a className from an existing string of classes.
10
+ *
11
+ * @param {string} existing - String of existing classes to work with.
12
+ * @param {string} className - Classname to add or remove
13
+ * @param {boolean} add - Either add or remove the class.
14
+ *
15
+ * @return {string} - The modified string of classes.
16
+ */
17
+ export function classNameAmend( existing: string | undefined, className: string, add: boolean ): string {
18
+ let classes: string[] = [];
19
+ if ( undefined !== existing ) {
20
+ classes = existing.split( ' ' );
21
+ }
22
+ if ( add ) {
23
+ classes.push( className );
24
+ } else {
25
+ classes = classes.filter( _class => _class !== className );
26
+ }
27
+ return [ ...new Set( classes ) ].join( ' ' );
28
+ }
29
+
30
+ /**
31
+ * Sanitize a CSS class name provided by PHP CSS module.
32
+ *
33
+ * - Removes any leading or trailing whitespace.
34
+ * - Replaces any spaces with a single dot.
35
+ * - Adds a leading dot if not present.
36
+ *
37
+ * @param {string} className - The class name to sanitize.
38
+ *
39
+ * @return {string} - The sanitized class name.
40
+ */
41
+ export function sanitizeCssModuleClass( className: string | null ): string {
42
+ if ( null === className ) {
43
+ return '';
44
+ }
45
+ className = className.trim();
46
+ className = className.replace( /\s+/g, '.' );
47
+ if ( className.charAt( 0 ) !== '.' ) {
48
+ className = '.' + className;
49
+ }
50
+ return className;
51
+ }
@@ -0,0 +1,28 @@
1
+ # colors
2
+
3
+ Convert between hex and RGB color formats and measure perceived lightness.
4
+
5
+ ## Exports
6
+
7
+ ### `hexToRGB( hex, alpha? ): string`
8
+
9
+ Convert a hex color to an `rgb()` string, or `rgba()` when `alpha` is provided. Non-hex input is returned unchanged.
10
+
11
+ ### `RGBToHex( rgb ): string`
12
+
13
+ Convert an `rgb()`/`rgba()` string to a hex color. Input already starting with `#` is returned unchanged. Returns `''` if it cannot be parsed.
14
+
15
+ ### `getLightness( hexOrRGB ): number`
16
+
17
+ Return the relative luminance of a hex or `rgb()` color as a number between `0` and `1`. `> 0.5` is light, `<= 0.5` is dark.
18
+
19
+ ## Usage
20
+
21
+ ```ts
22
+ import {hexToRGB, RGBToHex, getLightness} from '@lipemat/js-helpers';
23
+
24
+ hexToRGB( '#fff' ); // 'rgb(255, 255, 255)'
25
+ hexToRGB( '#000', 0.5 ); // 'rgba(0, 0, 0, 0.5)'
26
+ RGBToHex( 'rgb(255, 255, 255)' ); // '#ffffff'
27
+ getLightness( '#ffffff' ); // 1
28
+ ```
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Color Utilities
3
+ *
4
+ * @version 1.2.0
5
+ */
6
+
7
+ export function hexToRGB( hex: string, alpha?: number ): string {
8
+ if ( '#' !== hex[ 0 ] ) {
9
+ return hex;
10
+ }
11
+ const {r, g, b} = convertHexToRGB( hex );
12
+
13
+ if ( typeof alpha !== 'undefined' ) {
14
+ return 'rgba(' + r + ', ' + g + ', ' + b + ', ' + alpha + ')';
15
+ }
16
+ return 'rgb(' + r + ', ' + g + ', ' + b + ')';
17
+ }
18
+
19
+
20
+ export function RGBToHex( rgb: string ): string {
21
+ if ( '#' === rgb[ 0 ] ) {
22
+ return rgb;
23
+ }
24
+ const colors = rgb.match(
25
+ /^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i,
26
+ );
27
+ return ( colors !== null ) && 4 === colors.length
28
+ ? '#' +
29
+ ( '0' + parseInt( colors[ 1 ], 10 ).toString( 16 ) ).slice( -2 ) +
30
+ ( '0' + parseInt( colors[ 2 ], 10 ).toString( 16 ) ).slice( -2 ) +
31
+ ( '0' + parseInt( colors[ 3 ], 10 ).toString( 16 ) ).slice( -2 )
32
+ : '';
33
+ }
34
+
35
+ /**
36
+ * Get the lightness of a color.
37
+ *
38
+ * > 0.5 is light, <= 0.5 is dark.
39
+ */
40
+ export function getLightness( hexOrRGB: string ): number {
41
+ const {r, g, b} = convertHexToRGB( hexOrRGB );
42
+ return Number( ( ( ( 0.2126 * r ) + ( 0.7152 * g ) + ( 0.0722 * b ) ) / 255 ).toFixed( 2 ) );
43
+ }
44
+
45
+
46
+ function convertHexToRGB( hex: string ): { r: number, b: number, g: number } {
47
+ // Already RGB, just convert to object.
48
+ if ( hex.startsWith( 'rgb(' ) ) {
49
+ const colors = hex.match(
50
+ /^rgba?\(\s*(?<r>\d+)\s*,\s*(?<g>\d+)\s*,\s(?<b>\d+)(?:\s*,\s*[\d.]+)?\s*\)$/i,
51
+ );
52
+ const groups = colors?.groups;
53
+ return {
54
+ r: parseInt( groups?.r ?? '0', 10 ),
55
+ g: parseInt( groups?.g ?? '0', 10 ),
56
+ b: parseInt( groups?.b ?? '0', 10 ),
57
+ };
58
+ }
59
+
60
+
61
+ if ( 4 === hex.length ) {
62
+ let colors = hex.substring( 1 ).split( '' );
63
+ colors = [
64
+ colors[ 0 ],
65
+ colors[ 0 ],
66
+ colors[ 1 ],
67
+ colors[ 1 ],
68
+ colors[ 2 ],
69
+ colors[ 2 ],
70
+ ];
71
+ hex = colors.join( '' );
72
+ }
73
+
74
+ const r = parseInt( hex.slice( 1, 3 ), 16 ),
75
+ g = parseInt( hex.slice( 3, 5 ), 16 ),
76
+ b = parseInt( hex.slice( 5, 7 ), 16 );
77
+
78
+
79
+ return {r, g, b};
80
+ }
@@ -0,0 +1,35 @@
1
+ # csvExport
2
+
3
+ Generate a CSV file in the browser and trigger a download. HTML entities in the data are decoded and values are sanitized with [DOMPurify](https://github.com/cure53/DOMPurify).
4
+
5
+ ## Exports
6
+
7
+ ### `csvExport( data, fileName ): void`
8
+
9
+ Build a CSV from `data` and prompt the browser to download it as `fileName`.
10
+
11
+ - `data: CsvData` — an array of row objects (keys become headers) or an array of arrays (no header row).
12
+ - `fileName: string` — download file name.
13
+
14
+ ### `CsvData` (type)
15
+
16
+ ```ts
17
+ type CsvData = Array<{ [ column: string ]: number | string } | Array<number | string>>;
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ ```ts
23
+ import {csvExport, type CsvData} from '@lipemat/js-helpers';
24
+
25
+ const data: CsvData = [
26
+ {name: 'Jane', email: 'jane@example.com'},
27
+ {name: 'John', email: 'john@example.com'},
28
+ ];
29
+
30
+ csvExport( data, 'users.csv' );
31
+ ```
32
+
33
+ ## Notes
34
+
35
+ Requires a DOM (`document`, `Blob`, `URL.createObjectURL`); intended for browser use.
@@ -0,0 +1,89 @@
1
+ import DOMPurify from 'dompurify';
2
+
3
+ const {sanitize} = DOMPurify;
4
+
5
+ /**
6
+ * @version 1.0.8
7
+ */
8
+
9
+ export type CsvData = Array<{ [ column: string ]: number | string } | Array<number | string>>
10
+
11
+ /**
12
+ * Generate a csv file and trigger a download of a said file.
13
+ *
14
+ * @author Mat Lipe
15
+ * @since September, 2019
16
+ *
17
+ * @param {Array<Object>} data
18
+ * @param {string} fileName
19
+ *
20
+ * @return {void}
21
+ */
22
+ export function csvExport( data: CsvData, fileName: string ): void {
23
+ const csv = convertArrayOfObjectsToCSV( data );
24
+ if ( null === csv ) {
25
+ return;
26
+ }
27
+
28
+ const blob = new Blob( [ csv ], {type: 'text/csv;charset=utf-8;'} );
29
+ const link = document.createElement( 'a' );
30
+
31
+ if ( link.download !== undefined ) {
32
+ link.setAttribute( 'href', sanitize( URL.createObjectURL( blob ) ) );
33
+ link.setAttribute( 'download', sanitize( fileName ) );
34
+ link.style.visibility = 'hidden';
35
+ document.body.appendChild( link );
36
+ link.click();
37
+ document.body.removeChild( link );
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Convert an array of objects|array to a string representation of a csv file.
43
+ *
44
+ * @param data
45
+ */
46
+ function convertArrayOfObjectsToCSV( data: CsvData ): string | null {
47
+ if ( null === data || ( 0 === data.length ) ) {
48
+ return null;
49
+ }
50
+
51
+ const keys: string[] = Object.keys( data[ 0 ] );
52
+
53
+ let result = '';
54
+ if ( '0' !== keys[ 0 ] ) {
55
+ result += keys.join( ',' );
56
+ result += '\n';
57
+ }
58
+
59
+ data.forEach( item => {
60
+ keys.forEach( ( key: string, i: number ) => {
61
+ if ( i > 0 ) {
62
+ result += ',';
63
+ }
64
+ // @ts-expect-error
65
+ let innerValue = item[ key ]?.toString().replace( /"/g, '""' ) ?? '';
66
+ if ( innerValue.search( /([",\n])/g ) >= 0 ) {
67
+ innerValue = '"' + innerValue + '"';
68
+ }
69
+ result += decodeHtml( innerValue );
70
+ } );
71
+ result += '\n';
72
+ } );
73
+
74
+ return result;
75
+ }
76
+
77
+ /**
78
+ * Convert any HTML special characters to their string version,
79
+ * so they may be encoded via `encodeURI`.
80
+ *
81
+ * @param {string} html
82
+ *
83
+ * @return {string}
84
+ */
85
+ function decodeHtml( html: string ): string {
86
+ const txt = document.createElement( 'textarea' );
87
+ txt.innerHTML = sanitize( html );
88
+ return txt.value;
89
+ }
@@ -0,0 +1,21 @@
1
+ # date
2
+
3
+ Format a `Date` object using a small set of tokens.
4
+
5
+ ## Exports
6
+
7
+ ### `getFormattedDate( date, format? ): string`
8
+
9
+ Return `date` formatted using the `format` string. Supported tokens are `mm` (month), `dd` (day), and `yyyy` (year). Defaults to `'mm/dd/yyyy'`.
10
+
11
+ - `date: Date` — the date to format.
12
+ - `format?: string` — token string, defaults to `'mm/dd/yyyy'`.
13
+
14
+ ## Usage
15
+
16
+ ```ts
17
+ import {getFormattedDate} from '@lipemat/js-helpers';
18
+
19
+ getFormattedDate( new Date( 2023, 0, 5 ) ); // '01/05/2023'
20
+ getFormattedDate( new Date( 2023, 0, 5 ), 'yyyy-mm-dd' ); // '2023-01-05'
21
+ ```
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @version 1.1.0
3
+ */
4
+
5
+ /**
6
+ * Given a Date object, return the date in the provided format.
7
+ * Limited to `mm`, `dd`, and `yyyy` formats.
8
+ *
9
+ */
10
+ export function getFormattedDate( date: Date, format: string = 'mm/dd/yyyy' ): string {
11
+ const year = date.getFullYear();
12
+
13
+ let month = ( 1 + date.getMonth() ).toString();
14
+ month = month.length > 1 ? month : '0' + month;
15
+
16
+ let day = date.getDate().toString();
17
+ day = day.length > 1 ? day : '0' + day;
18
+
19
+ return format
20
+ .replace( 'mm', month )
21
+ .replace( 'dd', day )
22
+ .replace( 'yyyy', year.toString() );
23
+ }
@@ -0,0 +1,27 @@
1
+ # debounce
2
+
3
+ Lodash-free debounce. Delays calling `fn` until `wait` ms have passed since the last call, returning a promise that resolves with the result.
4
+
5
+ ## Exports
6
+
7
+ ### `debounce( fn, wait? )`
8
+
9
+ - `fn: T` — function to debounce.
10
+ - `wait?: number` — delay in milliseconds, defaults to `300`.
11
+
12
+ Returns a debounced function that resolves a `Promise` with the result, plus:
13
+
14
+ - `.immediate( ...args )` — run `fn` right away, bypassing the wait.
15
+ - `.cancel()` — cancel a pending call.
16
+
17
+ ## Usage
18
+
19
+ ```ts
20
+ import {debounce} from '@lipemat/js-helpers';
21
+
22
+ const save = debounce( ( value: string ) => persist( value ), 500 );
23
+
24
+ await save( 'hello' ); // resolves after 500ms of inactivity
25
+ save.immediate( 'now' ); // runs synchronously
26
+ save.cancel(); // cancel a pending call
27
+ ```
@@ -0,0 +1,47 @@
1
+ import type {Callback, Return} from '../once/once.js';
2
+
3
+ type Result<T extends Callback> = {
4
+ ( ...args: Parameters<T> ): Promise<ReturnType<T>>;
5
+ immediate: ( ...args: Parameters<T> ) => ReturnType<T>;
6
+ cancel: () => void;
7
+ };
8
+
9
+
10
+ /**
11
+ * Alternative to lodash debounce.
12
+ *
13
+ * - Additional support for `immediate` execution to bypass the wait time.
14
+ * - Returns a promise to allow waiting for a completion of the debounced function.
15
+ *
16
+ * @version 1.4.1
17
+ */
18
+ export function debounce<T extends Callback>( fn: T, wait = 300 ): Result<T> {
19
+ let timer: number | NodeJS.Timeout;
20
+ let promise: Promise<ReturnType<T>> | null = null;
21
+ let resolve: ( value: ReturnType<T> ) => void;
22
+
23
+ const debounced = function( ...args: Parameters<T> ): Promise<ReturnType<T>> {
24
+ clearTimeout( timer );
25
+ timer = setTimeout( () => {
26
+ resolve( fn( ...args ) );
27
+ }, wait );
28
+
29
+ return promise ??= new Promise<ReturnType<T>>( res => {
30
+ resolve = res;
31
+ } );
32
+ };
33
+
34
+ // Immediate execution method.
35
+ debounced.immediate = function( ...args: Parameters<T> ): Return<T> {
36
+ clearTimeout( timer );
37
+ return fn( ...args );
38
+ };
39
+
40
+ // Cancel method.
41
+ debounced.cancel = function() {
42
+ clearTimeout( timer );
43
+ promise = null;
44
+ };
45
+
46
+ return debounced as Result<T>;
47
+ }
@@ -0,0 +1,20 @@
1
+ # delay
2
+
3
+ Resolve a promise after a given amount of time. Useful for letting transitions finish while other work happens.
4
+
5
+ ## Exports
6
+
7
+ ### `delay( ms ): Promise<void>`
8
+
9
+ - `ms: number` — milliseconds to wait before resolving.
10
+
11
+ ## Usage
12
+
13
+ ```ts
14
+ import {delay} from '@lipemat/js-helpers';
15
+
16
+ const delayPromise = delay( 200 );
17
+ // Do other work while waiting.
18
+ await fetchPosts();
19
+ await delayPromise;
20
+ ```
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Delay a promise by a given amount of time.
3
+ *
4
+ * Used to provide enough time to show transitions while
5
+ * other operations are happening.
6
+ *
7
+ * @example ```ts
8
+ * const delayPromise = delay(200);
9
+ * // Do another action while waiting for the delay.
10
+ * await fetchPosts();
11
+ * await delayPromise;
12
+ *
13
+ * @param ms The amount of time to delay the promise by.
14
+ *
15
+ * @version 1.0.0
16
+ */
17
+ export const delay = ( ms: number ): Promise<void> => {
18
+ return new Promise( resolve => setTimeout( resolve, ms ) );
19
+ };
@@ -0,0 +1,31 @@
1
+ # device
2
+
3
+ Detect whether the current client is a desktop or mobile device. Modeled after WordPress' `wp_is_mobile()` but evaluated client-side so it works with edge page caching and user-agent emulation.
4
+
5
+ ## Exports
6
+
7
+ ### `isDesktop(): boolean`
8
+
9
+ `true` when the viewport is wider than 800px and the user agent is not mobile.
10
+
11
+ ### `isMobile(): boolean`
12
+
13
+ `true` when the viewport is 800px or narrower, or the user agent is mobile.
14
+
15
+ ### `hasMobileUserAgent(): boolean`
16
+
17
+ `true` when `navigator.userAgentData.mobile` is set, or the `navigator.userAgent` string matches a known mobile identifier.
18
+
19
+ ## Usage
20
+
21
+ ```ts
22
+ import {isDesktop, isMobile, hasMobileUserAgent} from '@lipemat/js-helpers';
23
+
24
+ if ( isMobile() ) {
25
+ // Render the compact layout.
26
+ }
27
+ ```
28
+
29
+ ## Notes
30
+
31
+ Requires `window` and `navigator`; intended for browser use.
@@ -0,0 +1,46 @@
1
+ const breakPoint = 800;
2
+
3
+
4
+ /**
5
+ * Detects if the user is on a desktop or mobile device.
6
+ * - Desktop: Width greater than 800 px and not a mobile user agent.
7
+ * - Mobile: Width less than 800 px or a mobile user agent.
8
+ *
9
+ * @see useMobile - For keeping track of mobile state in React components.
10
+ *
11
+ * @version 2.0.2
12
+ */
13
+
14
+
15
+ export function isDesktop(): boolean {
16
+ return window.innerWidth > breakPoint && ! hasMobileUserAgent();
17
+ }
18
+
19
+ export function isMobile(): boolean {
20
+ return window.innerWidth < breakPoint || hasMobileUserAgent();
21
+ }
22
+
23
+ /**
24
+ * Checks if the user agent is a mobile device.
25
+ * - Checks if an HTTPS `userAgentData` API is available and if the `mobile` property is true.
26
+ * - If not available, checks the `navigator.userAgent` string for common mobile identifiers.
27
+ *
28
+ * Modeled after the `wp_is_mobile()` function in WordPress but runs on the client side
29
+ * to support:
30
+ * 1. Edge page caching.
31
+ * 2. Changes in emulation of the user agent during development.
32
+ *
33
+ * @link https://developer.mozilla.org/en-US/docs/Web/API/Navigator/userAgentData
34
+ * @link https://developer.wordpress.org/reference/functions/wp_is_mobile/
35
+ */
36
+ export function hasMobileUserAgent(): boolean {
37
+ const ua = navigator.userAgent;
38
+ // @ts-expect-error TS2551 -- Not all browsers support userAgentData yet.
39
+ const uaData = navigator.userAgentData ?? undefined;
40
+ if ( 'undefined' !== typeof uaData && 'undefined' !== typeof uaData.mobile && Boolean( uaData.mobile ) ) {
41
+ return true;
42
+ } else if ( 'undefined' === typeof ua || '' === ua ) {
43
+ return false;
44
+ }
45
+ return [ 'Mobile', 'Android', 'Silk/', 'Kindle', 'BlackBerry', 'Opera Mini', 'Opera Mobi' ].some( agent => ua.includes( agent ) );
46
+ }
@@ -0,0 +1,27 @@
1
+ # dom-ready
2
+
3
+ Run a callback once the document has finished parsing. Logic borrowed from `@wordpress/dom-ready`.
4
+
5
+ ## Exports
6
+
7
+ ### `domReady( callback ): void`
8
+
9
+ Execute `callback` after `DOMContentLoaded`. If the document is already `interactive` or `complete`, the callback runs immediately. Does nothing when `document` is undefined (e.g. server-side).
10
+
11
+ - `callback: EventCallback` — function to run when the DOM is ready.
12
+
13
+ ### `EventCallback` (type)
14
+
15
+ ```ts
16
+ type EventCallback = ( this: Document | void, ev?: Event ) => void;
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ```ts
22
+ import {domReady} from '@lipemat/js-helpers';
23
+
24
+ domReady( () => {
25
+ console.log( 'DOM is ready' );
26
+ } );
27
+ ```
@@ -0,0 +1,22 @@
1
+ export type EventCallback = ( this: Document | void, ev?: Event ) => void;
2
+
3
+
4
+ /**
5
+ * Fire a callback after the DOMContentLoaded event.
6
+ * If called after the document is loaded, callback will be executed immediately.
7
+ * Logic borrowed from `@wordpress/dom-ready`.
8
+ *
9
+ * @param {Function} callback Callback to execute.
10
+ *
11
+ * @version 1.0.0
12
+ */
13
+ export function domReady( callback: EventCallback ) {
14
+ if ( 'undefined' === typeof document ) {
15
+ return;
16
+ }
17
+ if ( 'complete' === document.readyState || 'interactive' === document.readyState ) {
18
+ return void callback();
19
+ }
20
+
21
+ document.addEventListener( 'DOMContentLoaded', () => callback() );
22
+ }