@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
@@ -0,0 +1,40 @@
1
+ # error
2
+
3
+ An `Error` subclass that carries per-field messages. Useful for translating `WP_Error` REST API responses into form field errors.
4
+
5
+ ## Exports
6
+
7
+ ### `class ErrorWithFields<T>`
8
+
9
+ Extends `Error`. Accepts an `ErrorField<T>` (which may include `additional_errors`) and exposes a `fields` map of field code to message. Field codes are normalized (e.g. `user_email` → `email`).
10
+
11
+ ### `ErrorField<T>` (type)
12
+
13
+ ```ts
14
+ interface ErrorField<T> {
15
+ code: keyof T;
16
+ data?: object | string | boolean | number;
17
+ message: string;
18
+ additional_errors?: Array<ErrorField<T>>;
19
+ }
20
+ ```
21
+
22
+ ### `Fields<T>` (type)
23
+
24
+ ```ts
25
+ type Fields<T> = { [field in keyof T]?: string };
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ ```ts
31
+ import {ErrorWithFields} from '@lipemat/js-helpers';
32
+
33
+ type Form = {email: string; username: string};
34
+
35
+ throw new ErrorWithFields<Form>( {
36
+ code: 'rest_user_invalid_email',
37
+ message: 'Invalid email address.',
38
+ } );
39
+ // error.fields.email === 'Invalid email address.'
40
+ ```
@@ -0,0 +1,49 @@
1
+ export interface ErrorField<T extends { [ field: string ]: string }> {
2
+ code: keyof T;
3
+ data?: object | string | boolean | number;
4
+ message: string;
5
+
6
+ additional_errors?: Array<ErrorField<T>>;
7
+ }
8
+
9
+ export type Fields<T> = {
10
+ [field in keyof T]?: string;
11
+ };
12
+
13
+ /**
14
+ * Use in place from the default Error handler to include
15
+ * information about form fields which caused the errors.
16
+ *
17
+ * Translates `WP_Error` responses from REST API into
18
+ * field name and their corresponding messages.
19
+ *
20
+ * @version 1.2.1
21
+ */
22
+ export class ErrorWithFields<T extends { [ field: string ]: string }> extends Error {
23
+ public fields: Fields<T>;
24
+
25
+ constructor( error: ErrorField<T> ) {
26
+ const errors = [ error ];
27
+ if ( undefined !== error.additional_errors ) {
28
+ errors.push( ...error.additional_errors );
29
+ }
30
+
31
+ super( errors.map( er => er.message ).join( ' ' ) );
32
+
33
+ this.name = 'ErrorWithData';
34
+ this.fields = {};
35
+ errors.forEach( field => {
36
+ this.fields[ this.translate( field ).code ] = field.message;
37
+ } );
38
+ }
39
+
40
+ translate( error: ErrorField<T> ): ErrorField<T> {
41
+ error.code = error.code
42
+ .toString()
43
+ .replace( 'user_name', 'username' )
44
+ .replace( 'user_email', 'email' )
45
+ .replace( 'rest_user_invalid_email', 'email' );
46
+
47
+ return error;
48
+ }
49
+ }
@@ -0,0 +1,23 @@
1
+ # escaping
2
+
3
+ Decode HTML entities in a string. Taken nearly verbatim from Gutenberg's `@wordpress/html-entities`, with the input sanitized through [DOMPurify](https://github.com/cure53/DOMPurify).
4
+
5
+ ## Exports
6
+
7
+ ### `decodeEntities( html ): string`
8
+
9
+ Return `html` with HTML entities decoded. Strings without a `&` are returned unchanged. A reusable detached `<textarea>` is used for decoding.
10
+
11
+ - `html: string` — string that may contain HTML entities.
12
+
13
+ ## Usage
14
+
15
+ ```ts
16
+ import {decodeEntities} from '@lipemat/js-helpers';
17
+
18
+ decodeEntities( 'Ben &amp; Jerry&#039;s' ); // "Ben & Jerry's"
19
+ ```
20
+
21
+ ## Notes
22
+
23
+ Requires a DOM (`document`); intended for browser use.
@@ -0,0 +1,41 @@
1
+ import DOMPurify from 'dompurify';
2
+
3
+ let _decodeTextArea: HTMLTextAreaElement;
4
+
5
+ /**
6
+ * Taken nearly verbatim from the Gutenberg package.
7
+ *
8
+ * Added to the helpers in case we are
9
+ * not loading wp-html-entities on the front-end.
10
+ *
11
+ * @notice We likely are loading wp-html-entities unless a special project.
12
+ *
13
+ * @see @wordpress/html-entities
14
+ *
15
+ * @version 1.0.5
16
+ *
17
+ * @link https://github.com/WordPress/gutenberg/blob/d5915916abc45e6682f4bdb70888aa41e98aa395/packages/html-entities/src/index.js#L14
18
+ * @param html
19
+ */
20
+ export function decodeEntities( html: string ) {
21
+ // Not a string, or no entities to decode.
22
+ if ( -1 === html.indexOf( '&' ) ) {
23
+ return html;
24
+ }
25
+
26
+ // Create a textarea for decoding entities, that we can reuse.
27
+ if ( undefined === _decodeTextArea ) {
28
+ if ( undefined !== document.implementation ) {
29
+ _decodeTextArea = document.implementation
30
+ .createHTMLDocument( '' )
31
+ .createElement( 'textarea' );
32
+ } else {
33
+ _decodeTextArea = document.createElement( 'textarea' );
34
+ }
35
+ }
36
+
37
+ _decodeTextArea.innerHTML = DOMPurify.sanitize( html );
38
+ const decoded = _decodeTextArea.textContent;
39
+ _decodeTextArea.innerHTML = '';
40
+ return ( decoded );
41
+ }
package/src/index.ts ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Public entry point for `@lipemat/js-helpers`.
3
+ *
4
+ * Re-exports the public API of every helper so consumers can use
5
+ * `import {<helper>} from '@lipemat/js-helpers'`.
6
+ */
7
+ export * from './classes/classes.js';
8
+ export * from './colors/colors.js';
9
+ export * from './csvExport/csvExport.js';
10
+ export * from './date/date.js';
11
+ export * from './debounce/debounce.js';
12
+ export * from './delay/delay.js';
13
+ export * from './device/device.js';
14
+ export * from './dom-ready/dom-ready.js';
15
+ export * from './error/error.js';
16
+ export * from './escaping/escaping.js';
17
+ export * from './injectScript/injectScript.js';
18
+ export * from './memoize/memoize.js';
19
+ export * from './noop/noop.js';
20
+ export * from './objects/objects.js';
21
+ export * from './once/once.js';
22
+ export * from './string/string.js';
23
+ export * from './throttle/throttle.js';
24
+ export * from './url/url.js';
@@ -0,0 +1,26 @@
1
+ # injectScript
2
+
3
+ Inject an external script into the DOM once. Memoized by `src`, so repeated calls with the same URL reuse the same promise instead of adding the script again.
4
+
5
+ ## Exports
6
+
7
+ ### `injectScript( src, inBody? )`
8
+
9
+ - `src: string` — URL of the script to inject.
10
+ - `inBody?: boolean` — append to `<body>` instead of `<head>`. Defaults to `false`.
11
+
12
+ Returns a `Promise` that resolves when the script loads and rejects on error or abort.
13
+
14
+ ## Usage
15
+
16
+ ```ts
17
+ import {injectScript} from '@lipemat/js-helpers';
18
+
19
+ await injectScript( 'https://example.com/widget.js' );
20
+ // Calling again with the same URL reuses the first promise.
21
+ await injectScript( 'https://example.com/widget.js' );
22
+ ```
23
+
24
+ ## Notes
25
+
26
+ Requires a DOM (`document`); intended for browser use.
@@ -0,0 +1,31 @@
1
+ import {memoize} from '../memoize/memoize.js';
2
+
3
+ /**
4
+ * Inject a script into the DOM.
5
+ *
6
+ * May be called multiple times and will ony inject the script once.
7
+ *
8
+ * @param {string} $src - URL of the script.
9
+ * @param {boolean} $inBody - Put in body tag instead of head
10
+ *
11
+ * @version 1.2.1
12
+ *
13
+ * @return {Promise}
14
+ */
15
+ export const injectScript = memoize( ( src, inBody: boolean = false ) => {
16
+ return new Promise( ( resolve, reject ) => {
17
+ const script = document.createElement( 'script' );
18
+ script.async = false;
19
+ script.src = src;
20
+ script.addEventListener( 'load', () => resolve( 'Loaded: ' + src ) );
21
+ script.addEventListener( 'error', () => reject( 'Error loading script.' ) );
22
+ script.addEventListener( 'abort', () =>
23
+ reject( 'Script loading aborted.' ),
24
+ );
25
+ if ( inBody ) {
26
+ document.body.appendChild( script );
27
+ } else {
28
+ document.head.appendChild( script );
29
+ }
30
+ } );
31
+ } );
@@ -0,0 +1,28 @@
1
+ # memoize
2
+
3
+ Lodash-free memoization. Caches results of `fn` keyed by the first argument, or by a custom `resolver`.
4
+
5
+ ## Exports
6
+
7
+ ### `memoize( fn, resolver? )`
8
+
9
+ - `fn: T` — function whose results are cached.
10
+ - `resolver?: ( ...args ) => string` — derive a custom cache key. Defaults to the first argument.
11
+
12
+ Returns the memoized function with a `.cache` `Map` you can inspect or clear.
13
+
14
+ ### `MemoizedFunction<T>` (type)
15
+
16
+ The returned function type, including the `cache: Map<string, ReturnType<T>>` property.
17
+
18
+ ## Usage
19
+
20
+ ```ts
21
+ import {memoize} from '@lipemat/js-helpers';
22
+
23
+ const slugify = memoize( ( value: string ) => value.toLowerCase().replace( /\s+/g, '-' ) );
24
+
25
+ slugify( 'Hello World' ); // computed
26
+ slugify( 'Hello World' ); // from cache
27
+ slugify.cache.clear();
28
+ ```
@@ -0,0 +1,27 @@
1
+ import type {Callback, Return} from '../once/once.js';
2
+
3
+ type Resolver<T extends Callback> = ( ...args: Parameters<T> ) => string;
4
+
5
+ export type MemoizedFunction<T extends Callback> = Return<T> & {
6
+ cache: Map<string, ReturnType<T>>;
7
+ };
8
+
9
+ /**
10
+ * Similar to lodash memoize but without requiring lodash.
11
+ *
12
+ * @version 1.1.3
13
+ */
14
+ export function memoize<T extends Callback>( fn: T, resolver?: Resolver<T> ): MemoizedFunction<T> {
15
+ const memoized = function( ...args: Parameters<T> ): ReturnType<Callback> {
16
+ const key = resolver ? resolver( ...args ) : args[ 0 ];
17
+ if ( memoized.cache.has( key ) ) {
18
+ return memoized.cache.get( key );
19
+ }
20
+
21
+ const result = fn( ...args );
22
+ memoized.cache = memoized.cache.set( key, result );
23
+ return result;
24
+ };
25
+ memoized.cache = new Map();
26
+ return memoized;
27
+ }
@@ -0,0 +1,19 @@
1
+ # noop
2
+
3
+ A no-operation function. Lodash-free replacement for `lodash.noop`. Useful as a default callback.
4
+
5
+ ## Exports
6
+
7
+ ### `noop(): void`
8
+
9
+ Does nothing.
10
+
11
+ ## Usage
12
+
13
+ ```ts
14
+ import {noop} from '@lipemat/js-helpers';
15
+
16
+ function setup( {onChange = noop} = {} ) {
17
+ onChange();
18
+ }
19
+ ```
@@ -0,0 +1,9 @@
1
+ /**
2
+ * No operation function.
3
+ *
4
+ * Similar to lodash noop but without requiring lodash.
5
+ *
6
+ * @version 1.0.0
7
+ */
8
+ export const noop = () => {
9
+ };
@@ -0,0 +1,31 @@
1
+ # objects
2
+
3
+ Type-safe wrappers around `Object.keys` and `Object.entries` that preserve key types.
4
+
5
+ > More robust versions are available from the `@lipemat/js-boilerplate-gutenberg` package.
6
+
7
+ ## Exports
8
+
9
+ ### `keysOf( obj ): Array<keyof T>`
10
+
11
+ Like `Object.keys` but typed as `Array<keyof T>` instead of `string[]`.
12
+
13
+ ### `entriesOf( obj ): Array<[keyof T, T[keyof T]]>`
14
+
15
+ Like `Object.entries` but typed as `Array<[keyof T, T[keyof T]]>`.
16
+
17
+ ## Usage
18
+
19
+ ```ts
20
+ import {keysOf, entriesOf} from '@lipemat/js-helpers';
21
+
22
+ const config = {width: 100, label: 'box'};
23
+
24
+ keysOf( config ).forEach( key => {
25
+ // key is 'width' | 'label'
26
+ } );
27
+
28
+ entriesOf( config ).forEach( ( [ key, value ] ) => {
29
+ // fully typed key/value pair
30
+ } );
31
+ ```
@@ -0,0 +1,26 @@
1
+ /**
2
+ * @notice More robust versions are available from the `@lipemat/js-boilerplate-gutenberg` package.
3
+ */
4
+
5
+ /**
6
+ * Object.keys() with type inference.
7
+ *
8
+ * Instead of getting `string[]` we get `keyof T[]`.
9
+ *
10
+ * @since 4.3.0
11
+ */
12
+ export function keysOf<T extends object>( obj: T ): Array<keyof T> {
13
+ return Object.keys( obj ) as Array<keyof T>;
14
+ }
15
+
16
+
17
+ /**
18
+ * Object.entries() with type inference.
19
+ *
20
+ * Instead of getting `Array<[string, any]>` we get `Array<[keyof T, T[keyof T]]>`.
21
+ *
22
+ * @since 4.3.0
23
+ */
24
+ export function entriesOf<T extends object>( obj: T ): Array<[ keyof T, T[keyof T] ]> {
25
+ return Object.entries( obj ) as Array<[ keyof T, T[keyof T] ]>;
26
+ }
@@ -0,0 +1,26 @@
1
+ # once
2
+
3
+ Lodash-free `once`. Wrap a function so it only runs the first time; subsequent calls return the cached result.
4
+
5
+ ## Exports
6
+
7
+ ### `once( fn ): Return<T>`
8
+
9
+ - `fn: T` — function to run a single time.
10
+
11
+ Returns a function with the same signature that caches and returns the first result.
12
+
13
+ ### `Callback` / `Return<T>` (types)
14
+
15
+ Shared function-shape types used by `once`, `debounce`, `throttle`, and `memoize`.
16
+
17
+ ## Usage
18
+
19
+ ```ts
20
+ import {once} from '@lipemat/js-helpers';
21
+
22
+ const init = once( () => expensiveSetup() );
23
+
24
+ init(); // runs expensiveSetup()
25
+ init(); // returns the first result, no re-run
26
+ ```
@@ -0,0 +1,22 @@
1
+ // eslint-disable-next-line -- Only being used for a shape to guarantee the other types.
2
+ export type Callback = ( ...args: Array<any> ) => any;
3
+ export type Return<T extends Callback> = ( ...args: Parameters<T> ) => ReturnType<T>
4
+
5
+ /**
6
+ * Similar to lodash once but without requiring lodash.
7
+ *
8
+ * @version 1.0.2
9
+ */
10
+ export function once<T extends Callback>( fn: T ): Return<T> {
11
+ let result: ReturnType<T>;
12
+ let called = false;
13
+
14
+ return ( ...args: Parameters<T> ) => {
15
+ if ( ! called ) {
16
+ called = true;
17
+ result = fn( ...args );
18
+ }
19
+
20
+ return result;
21
+ };
22
+ }
@@ -0,0 +1,37 @@
1
+ # string
2
+
3
+ Small string helpers for random keys and slash management.
4
+
5
+ > More robust versions are available from the `@lipemat/js-boilerplate-gutenberg` package.
6
+
7
+ ## Exports
8
+
9
+ ### `generateRandomKey( length? ): string`
10
+
11
+ Return a random alphanumeric string. `length` defaults to `10`.
12
+
13
+ ### `addLeadingSlash( url ): string`
14
+
15
+ Ensure `url` starts with a slash. Empty/whitespace input is returned unchanged.
16
+
17
+ ### `addTrailingSlash( url ): string`
18
+
19
+ Ensure `url` ends with a slash. Empty/whitespace input is returned unchanged.
20
+
21
+ ### `removeLeadingSlash( url ): string`
22
+
23
+ Remove a leading slash if present. Empty/whitespace input is returned unchanged.
24
+
25
+ ### `removeTrailingSlash( url ): string`
26
+
27
+ Remove a trailing slash if present. Empty/whitespace input is returned unchanged.
28
+
29
+ ## Usage
30
+
31
+ ```ts
32
+ import {generateRandomKey, addTrailingSlash, removeLeadingSlash} from '@lipemat/js-helpers';
33
+
34
+ generateRandomKey(); // e.g. 'a8Kd0Lm2Qz'
35
+ addTrailingSlash( 'path/to' ); // 'path/to/'
36
+ removeLeadingSlash( '/path' ); // 'path'
37
+ ```
@@ -0,0 +1,72 @@
1
+ /**
2
+ * @notice More robust versions are available from the `@lipemat/js-boilerplate-gutenberg` package.
3
+ */
4
+
5
+ /**
6
+ * Generate a random key of a given length.
7
+ *
8
+ * @version 1.0.0
9
+ */
10
+ export function generateRandomKey( length: number = 10 ) {
11
+ const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
12
+ let result = '';
13
+ for ( let i = 0; i < length; i++ ) {
14
+ result += characters.charAt( Math.floor( Math.random() * characters.length ) );
15
+ }
16
+ return result;
17
+ }
18
+
19
+
20
+ /**
21
+ * Adds a leading slash to a URL if it doesn't already have one.
22
+ *
23
+ * @version 1.0.0
24
+ */
25
+ export function addLeadingSlash( url: string ): string {
26
+ const trimmedURL = url.trim();
27
+ if ( '' === trimmedURL ) {
28
+ return url;
29
+ }
30
+ return url?.replace( /^\/?/, '/' );
31
+ }
32
+
33
+
34
+ /**
35
+ * Adds a trailing slash to a URL if it doesn't already have one.
36
+ *
37
+ * @version 1.0.0
38
+ */
39
+ export function addTrailingSlash( url: string ): string {
40
+ const trimmedURL = url.trim();
41
+ if ( '' === trimmedURL ) {
42
+ return url;
43
+ }
44
+ return url.replace( /\/?$/, '/' );
45
+ }
46
+
47
+
48
+ /**
49
+ * Removes a leading slash from a URL if it has one.
50
+ *
51
+ * @version 1.0.0
52
+ */
53
+ export function removeLeadingSlash( url: string ): string {
54
+ const trimmedURL = url.trim();
55
+ if ( '' === trimmedURL ) {
56
+ return url;
57
+ }
58
+ return url.replace( /^\//, '' );
59
+ }
60
+
61
+ /**
62
+ * Removes a trailing slash from a URL if it has one.
63
+ *
64
+ * @version 1.0.0
65
+ */
66
+ export function removeTrailingSlash( url: string ): string {
67
+ const trimmedURL = url.trim();
68
+ if ( '' === trimmedURL ) {
69
+ return url;
70
+ }
71
+ return url.replace( /\/$/, '' );
72
+ }
@@ -0,0 +1,27 @@
1
+ # throttle
2
+
3
+ Lodash-free throttle. Ensures `fn` runs at most once per `wait` ms.
4
+
5
+ ## Exports
6
+
7
+ ### `throttle( fn, wait? )`
8
+
9
+ - `fn: T` — function to throttle.
10
+ - `wait?: number` — minimum delay between calls in milliseconds, defaults to `300`.
11
+
12
+ Returns a throttled function plus:
13
+
14
+ - `.immediate( ...args )` — run `fn` right away, bypassing the wait.
15
+ - `.cancel()` — cancel a pending trailing call.
16
+
17
+ ## Usage
18
+
19
+ ```ts
20
+ import {throttle} from '@lipemat/js-helpers';
21
+
22
+ const onScroll = throttle( () => update(), 100 );
23
+ window.addEventListener( 'scroll', onScroll );
24
+
25
+ onScroll.immediate(); // run now
26
+ onScroll.cancel(); // cancel a pending call
27
+ ```
@@ -0,0 +1,57 @@
1
+ import type {Callback, Return} from '../once/once.js';
2
+
3
+ type Result<T extends Callback> = {
4
+ ( ...args: Parameters<T> ): ReturnType<T>;
5
+ immediate: ( ...args: Parameters<T> ) => ReturnType<T>;
6
+ cancel: () => void;
7
+ };
8
+
9
+ /**
10
+ * Alternative to lodash throttle.
11
+ *
12
+ * - Additional support for `immediate` execution to bypass the wait time.
13
+ * - Returns a promise to allow waiting for a completion of the throttled function.
14
+ *
15
+ * @version 1.0.1
16
+ */
17
+ export function throttle<T extends Callback>( fn: T, wait = 300 ): Result<T> {
18
+ let lastTime = 0;
19
+ let timer: number | NodeJS.Timeout | undefined;
20
+ let result: ReturnType<T>;
21
+
22
+ const throttled = function( ...args: Parameters<T> ): Promise<ReturnType<T>> {
23
+ const now = Date.now();
24
+ const remaining = now - lastTime;
25
+
26
+ if ( remaining >= wait ) {
27
+ clearTimeout( timer );
28
+ timer = undefined;
29
+ lastTime = now;
30
+ return fn( ...args );
31
+ }
32
+
33
+ if ( 'undefined' === typeof timer ) {
34
+ timer = setTimeout( () => {
35
+ lastTime = Date.now();
36
+ result = fn( ...args );
37
+ timer = undefined;
38
+ }, wait - remaining );
39
+ }
40
+
41
+ return result;
42
+ };
43
+
44
+ // Immediate execution method.
45
+ throttled.immediate = function( ...args: Parameters<T> ): Return<T> {
46
+ clearTimeout( timer );
47
+ lastTime = Date.now();
48
+ return fn( ...args );
49
+ };
50
+
51
+ // Cancel method.
52
+ throttled.cancel = function() {
53
+ clearTimeout( timer );
54
+ };
55
+
56
+ return throttled as Result<T>;
57
+ }
@@ -0,0 +1,32 @@
1
+ # url
2
+
3
+ Read and append URL query arguments. Inputs are sanitized through [DOMPurify](https://github.com/cure53/DOMPurify).
4
+
5
+ > More robust versions are available from the `@lipemat/js-boilerplate-gutenberg` and `@wordpress/url` packages.
6
+
7
+ ## Exports
8
+
9
+ ### `getUrlParam( parameter, defaultValue? ): string | null`
10
+
11
+ Return the value of `parameter` from the current `window.location.search`, or `defaultValue` (default `''`) when it is absent.
12
+
13
+ ### `addQueryArgs( url?, args ): string`
14
+
15
+ Append `args` to `url` as query parameters and return the resulting URL string.
16
+
17
+ - `url?: string` — base URL, defaults to `''`.
18
+ - `args: { [name: string]: string | number }` — parameters to append.
19
+
20
+ ## Usage
21
+
22
+ ```ts
23
+ import {getUrlParam, addQueryArgs} from '@lipemat/js-helpers';
24
+
25
+ getUrlParam( 'page', '1' ); // current ?page value or '1'
26
+ addQueryArgs( 'https://example.com', {page: 2, sort: 'name'} );
27
+ // 'https://example.com/?page=2&sort=name'
28
+ ```
29
+
30
+ ## Notes
31
+
32
+ `getUrlParam` requires `window`; intended for browser use.