@splendidlabz/utils 1.5.0 → 1.5.1

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 (156) hide show
  1. package/CHANGELOG.md +71 -1
  2. package/dist/dom/accessibility.d.ts +28 -0
  3. package/dist/dom/actions/index.d.ts +3 -0
  4. package/dist/dom/actions/masonry.d.ts +3 -0
  5. package/dist/dom/actions/prefer-horizontal-scroll.d.ts +3 -0
  6. package/dist/dom/actions/sticky.d.ts +3 -0
  7. package/dist/dom/bounding-box.d.ts +39 -0
  8. package/dist/dom/clipboard.d.ts +5 -0
  9. package/dist/dom/cookie.d.ts +11 -0
  10. package/dist/dom/css-vars.d.ts +4 -0
  11. package/dist/dom/events.d.ts +18 -0
  12. package/dist/dom/focusable.d.ts +76 -0
  13. package/dist/dom/font-size.d.ts +5 -0
  14. package/dist/dom/get-element.d.ts +100 -0
  15. package/dist/dom/hash.d.ts +1 -0
  16. package/dist/dom/index.d.ts +22 -0
  17. package/dist/dom/keyboard.d.ts +30 -0
  18. package/dist/dom/local-store.d.ts +29 -0
  19. package/dist/dom/media.d.ts +3 -0
  20. package/dist/dom/observers/index.d.ts +3 -0
  21. package/dist/dom/observers/intersection-observer.d.ts +8 -0
  22. package/dist/dom/observers/mutation-observer.d.ts +7 -0
  23. package/dist/dom/observers/observer.d.ts +1 -0
  24. package/dist/dom/observers/resize-observer.d.ts +38 -0
  25. package/dist/dom/pkce.d.ts +6 -0
  26. package/dist/dom/query-params.d.ts +4 -0
  27. package/dist/dom/random-string.d.ts +7 -0
  28. package/dist/dom/sanitize.d.ts +8 -0
  29. package/dist/dom/session-store.d.ts +5 -0
  30. package/dist/dom/trap-focus.d.ts +28 -0
  31. package/dist/dom/ui/aria-current.d.ts +5 -0
  32. package/dist/dom/ui/inconsistent-button-fix.d.ts +12 -0
  33. package/dist/dom/ui/index.d.ts +4 -0
  34. package/dist/dom/ui/scroll-container.d.ts +1 -0
  35. package/dist/dom/ui/traverse-and-scramble.d.ts +19 -0
  36. package/dist/lib/arrays/index.d.ts +5 -0
  37. package/dist/lib/arrays/item-position.d.ts +14 -0
  38. package/dist/lib/arrays/join.d.ts +13 -0
  39. package/dist/lib/arrays/shuffle.d.ts +1 -0
  40. package/dist/lib/arrays/sort.d.ts +30 -0
  41. package/dist/lib/arrays/unique.d.ts +1 -0
  42. package/dist/lib/auth/index.d.ts +1 -0
  43. package/dist/lib/auth/route-manager.d.ts +25 -0
  44. package/dist/lib/checks.d.ts +29 -0
  45. package/dist/lib/date/days.d.ts +5 -0
  46. package/dist/lib/date/index.d.ts +3 -0
  47. package/dist/lib/date/months.d.ts +9 -0
  48. package/dist/lib/date/time.d.ts +8 -0
  49. package/dist/lib/form/form-data.d.ts +7 -0
  50. package/dist/lib/form/index.d.ts +2 -0
  51. package/dist/lib/form/sanitize.d.ts +58 -0
  52. package/dist/lib/functions/debounce.d.ts +1 -0
  53. package/dist/lib/functions/env.d.ts +1 -0
  54. package/dist/lib/functions/functional.d.ts +4 -0
  55. package/dist/lib/functions/index.d.ts +5 -0
  56. package/dist/lib/functions/throttle.d.ts +1 -0
  57. package/dist/lib/functions/timeout.d.ts +3 -0
  58. package/dist/lib/index.d.ts +13 -0
  59. package/dist/lib/numbers/index.d.ts +2 -0
  60. package/dist/lib/numbers/math.d.ts +2 -0
  61. package/dist/lib/objects/camelcase-keys.d.ts +1 -0
  62. package/dist/lib/objects/empty.d.ts +2 -0
  63. package/dist/lib/objects/equal.d.ts +2 -0
  64. package/dist/lib/objects/extend.d.ts +4 -0
  65. package/dist/lib/objects/flatten.d.ts +4 -0
  66. package/dist/lib/objects/index.d.ts +13 -0
  67. package/dist/lib/objects/json.d.ts +1 -0
  68. package/dist/lib/objects/loop.d.ts +2 -0
  69. package/dist/lib/objects/mix/mix.d.ts +1 -0
  70. package/dist/lib/objects/nested-property.d.ts +22 -0
  71. package/dist/lib/objects/normalize-object.d.ts +1 -0
  72. package/dist/lib/objects/omit-empty.d.ts +1 -0
  73. package/dist/lib/objects/size.d.ts +1 -0
  74. package/dist/lib/objects/split.d.ts +10 -0
  75. package/dist/lib/promises/index.d.ts +1 -0
  76. package/dist/lib/promises/reject.d.ts +3 -0
  77. package/dist/lib/sse.d.ts +16 -0
  78. package/dist/lib/strings/convert-case/convert-case.d.ts +8 -0
  79. package/dist/lib/strings/index.d.ts +4 -0
  80. package/dist/lib/strings/markdown.d.ts +23 -0
  81. package/dist/lib/strings/pluralize.d.ts +1 -0
  82. package/dist/lib/strings/query-string.d.ts +8 -0
  83. package/dist/lib/style/index.d.ts +1 -0
  84. package/dist/lib/symbols/index.d.ts +1 -0
  85. package/dist/lib/symbols/symbols.d.ts +2 -0
  86. package/dist/node/common.d.ts +1 -0
  87. package/dist/node/dirname.d.ts +7 -0
  88. package/dist/node/file-cache.d.ts +54 -0
  89. package/dist/node/file.d.ts +2 -0
  90. package/dist/node/hash.d.ts +1 -0
  91. package/dist/node/index.d.ts +7 -0
  92. package/dist/node/pkce.d.ts +6 -0
  93. package/dist/node/random-string.d.ts +2 -0
  94. package/dist/node/sanitize.d.ts +8 -0
  95. package/dom/accessibility.js +46 -13
  96. package/{actions → dom/actions}/index.js +0 -2
  97. package/dom/actions/masonry.js +42 -0
  98. package/{actions → dom/actions}/prefer-horizontal-scroll.js +8 -8
  99. package/{actions → dom/actions}/sticky.js +3 -6
  100. package/dom/bounding-box.js +35 -7
  101. package/dom/css-vars.js +8 -0
  102. package/dom/events.js +2 -1
  103. package/dom/focusable.js +119 -10
  104. package/dom/font-size.js +21 -10
  105. package/dom/get-element.js +112 -5
  106. package/dom/index.js +3 -0
  107. package/dom/keyboard.js +13 -8
  108. package/dom/local-store.js +37 -0
  109. package/dom/media.js +1 -0
  110. package/dom/observers/index.js +3 -0
  111. package/dom/observers/intersection-observer.js +43 -0
  112. package/dom/observers/mutation-observer.js +51 -0
  113. package/dom/observers/observer.js +18 -0
  114. package/dom/observers/resize-observer.js +64 -0
  115. package/dom/sanitize.js +16 -0
  116. package/dom/trap-focus.js +18 -3
  117. package/dom/ui/inconsistent-button-fix.js +14 -2
  118. package/dom/ui/traverse-and-scramble.js +29 -8
  119. package/eslint.config.js +2 -0
  120. package/lib/arrays/index.js +5 -62
  121. package/lib/arrays/item-position.js +23 -0
  122. package/lib/arrays/join.js +20 -0
  123. package/lib/arrays/shuffle.js +21 -0
  124. package/lib/arrays/sort.js +88 -0
  125. package/lib/arrays/sort.spec.js +123 -0
  126. package/lib/arrays/unique.js +3 -0
  127. package/lib/checks.js +26 -0
  128. package/lib/date/index.js +2 -1
  129. package/lib/date/time.js +50 -0
  130. package/lib/form/sanitize.js +68 -35
  131. package/lib/form/sanitize.spec.js +126 -0
  132. package/lib/index.js +1 -0
  133. package/lib/numbers/index.js +2 -0
  134. package/lib/numbers/math.js +9 -0
  135. package/lib/objects/equal.js +9 -0
  136. package/lib/objects/index.js +1 -0
  137. package/lib/objects/mix/mix.js +2 -0
  138. package/lib/objects/mix/mix.test.js +16 -0
  139. package/lib/objects/nested-property.js +5 -0
  140. package/lib/sse.js +61 -0
  141. package/lib/strings/markdown.js +29 -10
  142. package/node/dirname.js +1 -1
  143. package/node/file-cache.js +46 -7
  144. package/node/index.js +1 -0
  145. package/node/sanitize.js +13 -0
  146. package/node/sanitize.spec.js +142 -0
  147. package/package.json +38 -13
  148. package/tsconfig.json +23 -0
  149. package/.eslintrc.cjs +0 -3
  150. package/.turbo/turbo-lint.log +0 -10
  151. package/.turbo/turbo-test.log +0 -10
  152. package/actions/intersection-observer.js +0 -37
  153. package/actions/masonry.js +0 -75
  154. package/actions/mutation-observer.js +0 -42
  155. package/actions/resize-observer.js +0 -33
  156. package/lib/index.test.js +0 -9
package/dom/keyboard.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Event is Tab (without shift)
3
- * @param {Object} event - The event object
4
- * @returns {Boolean}
3
+ * @param {KeyboardEvent} event - The keyboard event object
4
+ * @returns {boolean}
5
5
  */
6
6
  export function isTab(event) {
7
7
  return !event.shiftKey && event.key === 'Tab'
@@ -9,8 +9,8 @@ export function isTab(event) {
9
9
 
10
10
  /**
11
11
  * Event is shift + Tab
12
- * @param {Object} event - The event object
13
- * @returns {Boolean}
12
+ * @param {KeyboardEvent} event - The keyboard event object
13
+ * @returns {boolean}
14
14
  */
15
15
  export function isShiftTab(event) {
16
16
  return event.shiftKey && event.key === 'Tab'
@@ -18,8 +18,8 @@ export function isShiftTab(event) {
18
18
 
19
19
  /**
20
20
  * Matches both Tab and Shift+Tab
21
- * @param {Object} event - The event object
22
- * @returns
21
+ * @param {string} key - The keyboard key to check
22
+ * @returns {boolean} Whether the key is Tab
23
23
  */
24
24
  export function isTabKey(key) {
25
25
  return key === 'Tab'
@@ -27,13 +27,18 @@ export function isTabKey(key) {
27
27
 
28
28
  /**
29
29
  * Event is an arrow key
30
- * @param {Object} event - The event object
31
- * @returns {Boolean}
30
+ * @param {string} key - The keyboard key to check
31
+ * @returns {boolean} Whether the key is an arrow key (Up, Down, Left, Right)
32
32
  */
33
33
  export function isArrowKey(key) {
34
34
  return ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(key)
35
35
  }
36
36
 
37
+ /**
38
+ * Event is Escape key
39
+ * @param {string} key - The keyboard key to check
40
+ * @returns {boolean} Whether the key is Escape
41
+ */
37
42
  export function isEscapeKey(key) {
38
43
  return key === 'Escape'
39
44
  }
@@ -1,7 +1,17 @@
1
1
  // ========================
2
2
  // Local Storage
3
3
  // ========================
4
+
5
+ /**
6
+ * Utility object for managing localStorage operations with JSON support
7
+ * @namespace localStore
8
+ */
4
9
  export const localStore = {
10
+ /**
11
+ * Retrieves a value from localStorage
12
+ * @param {string} key - The key to retrieve from localStorage
13
+ * @returns {any} The stored value. Returns parsed JSON if the value was stored as JSON. Returns the original string if not JSON, or undefined if key doesn't exist
14
+ */
5
15
  get(key) {
6
16
  const value = localStorage.getItem(key)
7
17
  if (!value) return
@@ -15,6 +25,11 @@ export const localStore = {
15
25
  }
16
26
  },
17
27
 
28
+ /**
29
+ * Stores a value in localStorage
30
+ * @param {string} key - The key to store the value under
31
+ * @param {any} value - The value to store. Objects will be stringified to JSON
32
+ */
18
33
  set(key, value) {
19
34
  if (typeof value === 'string') {
20
35
  localStorage.setItem(key, value)
@@ -23,7 +38,29 @@ export const localStore = {
23
38
  }
24
39
  },
25
40
 
41
+ /**
42
+ * Stores multiple key-value pairs in localStorage
43
+ * @param {Object|Map} data - An object or Map containing key-value pairs to store
44
+ */
45
+ setMultiple(data) {
46
+ for (const [key, value] of Object.entries(data)) {
47
+ this.set(key, value)
48
+ }
49
+ },
50
+
51
+ /**
52
+ * Removes a value from localStorage
53
+ * @param {string} key - The key to remove from localStorage
54
+ */
26
55
  remove(key) {
27
56
  localStorage.removeItem(key)
28
57
  },
58
+
59
+ /**
60
+ * Removes multiple keys from localStorage
61
+ * @param {string[]} keys - Array of keys to remove from localStorage
62
+ */
63
+ removeMultiple(keys) {
64
+ keys.forEach(key => this.remove(key))
65
+ },
29
66
  }
package/dom/media.js CHANGED
@@ -5,6 +5,7 @@ export async function imagesLoaded(node) {
5
5
  if (img.complete) return resolve()
6
6
  img.onload = resolve
7
7
  img.onerror = e => {
8
+ // eslint-disable-next-line prefer-promise-reject-errors
8
9
  reject('Image failed to load')
9
10
  }
10
11
  })
@@ -0,0 +1,3 @@
1
+ export * from './intersection-observer.js'
2
+ export * from './mutation-observer.js'
3
+ export * from './resize-observer.js'
@@ -0,0 +1,43 @@
1
+ import { dispatchEvent } from '../events.js'
2
+ import { useObserverMethodOnTarget } from './observer.js'
3
+
4
+ // See https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/IntersectionObserver#options for options
5
+ export function intersectionObserver(target, options = {}) {
6
+ const { callback, ...opts } = options
7
+ const observer = new IntersectionObserver(observerFn, opts)
8
+
9
+ useObserverMethodOnTarget(target, observer, 'observe')
10
+
11
+ function observerFn(entries) {
12
+ for (const entry of entries) {
13
+ if (callback) callback({ entry, entries, observer })
14
+ else dispatchEvent(target, 'intersect', { entry, entries, observer })
15
+ }
16
+ }
17
+
18
+ return {
19
+ observer,
20
+ observe(target) {
21
+ useObserverMethodOnTarget(target, observer, 'observe', options)
22
+ },
23
+
24
+ unobserve(target) {
25
+ useObserverMethodOnTarget(target, observer, 'unobserve')
26
+ },
27
+
28
+ takeRecords() {
29
+ return observer.takeRecords()
30
+ },
31
+
32
+ disconnect() {
33
+ // Take records before disconnecting.
34
+ const records = observer.takeRecords()
35
+ observer.disconnect()
36
+ if (records.length > 0) observerFn(records)
37
+ },
38
+
39
+ destroy() {
40
+ observer.disconnect()
41
+ },
42
+ }
43
+ }
@@ -0,0 +1,51 @@
1
+ /* eslint-env browser */
2
+ import { dispatchEvent } from '../events.js'
3
+ import { useObserverMethodOnTarget } from './observer.js'
4
+
5
+ // https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/observe
6
+
7
+ // These are simply options that will often be used. Check the docs for other options. Adding them here simply because it's easier to refer them here than reading the docs.
8
+ const defaultOptions = {
9
+ attributes: false,
10
+ childList: false,
11
+ subtree: false,
12
+ }
13
+
14
+ export function mutationObserver(target, options) {
15
+ options = { ...defaultOptions, ...options }
16
+ const { callback, ...opts } = options
17
+ const observer = new MutationObserver(observerFn)
18
+
19
+ if (target === window) target = document.body
20
+ useObserverMethodOnTarget(target, observer, 'observe', opts)
21
+
22
+ function observerFn(entries) {
23
+ for (const entry of entries) {
24
+ if (callback) callback({ entry, entries, observer })
25
+ else dispatchEvent(target, 'mutate', { entry, entries, observer })
26
+ }
27
+ }
28
+
29
+ return {
30
+ observer,
31
+ observe(target) {
32
+ useObserverMethodOnTarget(target, observer, 'observe', options)
33
+ },
34
+
35
+ takeRecords() {
36
+ return observer.takeRecords()
37
+ },
38
+
39
+ disconnect() {
40
+ // Take records before disconnecting.
41
+ // https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/takeRecords
42
+ const records = observer.takeRecords()
43
+ observer.disconnect()
44
+ if (records.length > 0) observerFn(records)
45
+ },
46
+
47
+ destroy() {
48
+ observer.disconnect()
49
+ },
50
+ }
51
+ }
@@ -0,0 +1,18 @@
1
+ import { getNodeType } from '../get-element.js'
2
+
3
+ export function useObserverMethodOnTarget(
4
+ target,
5
+ observer,
6
+ method = 'observe',
7
+ options = undefined
8
+ ) {
9
+ const targetType = getNodeType(target)
10
+ if (targetType === 'element') observer[method](target, options)
11
+ if (targetType === 'nodelist') {
12
+ const elements = Array.from(target)
13
+ elements.forEach(element => observer[method](element, options))
14
+ }
15
+ if (targetType === 'array') {
16
+ target.forEach(element => observer[method](element, options))
17
+ }
18
+ }
@@ -0,0 +1,64 @@
1
+ /* eslint-env browser */
2
+ import { dispatchEvent } from '../events.js'
3
+ import { useObserverMethodOnTarget } from './observer.js'
4
+
5
+ /**
6
+ * Creates and manages a ResizeObserver instance to monitor size changes of a target element.
7
+ *
8
+ * @param {Element|Window|NodeList|Element[]} target - The element(s) to observe.
9
+ * - If window is provided, document.body will be observed instead.
10
+ * - If NodeList or Array of elements is provided, all elements will be observed.
11
+ * @param {Object} options - Configuration options for the resize observer
12
+ * @param {boolean} [options.observe=true] - Whether to start observing immediately. If false, the observer won't be created.
13
+ * @param {Function} [options.callback] - Optional callback function that will be called when resize changes are detected.
14
+ * If not provided, a 'resize-obs' event will be dispatched on the target.
15
+ * @param {Object} [options.observerOptions] - Additional options to pass to ResizeObserver.observe()
16
+ *
17
+ * @returns {Object} An object with methods to control the observer:
18
+ * - observe(target, options): Start observing a new target element
19
+ * - unobserve(target): Stop observing a target element
20
+ * - disconnect(): Disconnect the observer and stop all observations
21
+ * - destroy(): Alias for disconnect()
22
+ *
23
+ * @example
24
+ * // Basic usage with callback
25
+ * resizeObserver(element, {
26
+ * callback: ({ entry, entries, observer }) => {
27
+ * console.log('Element resized:', entry.contentRect);
28
+ * }
29
+ * });
30
+ *
31
+ * @example
32
+ * // Usage with event listener
33
+ * resizeObserver(element);
34
+ * element.addEventListener('resize-obs', ({ detail }) => {
35
+ * console.log('Element resized:', detail.entry.contentRect);
36
+ * });
37
+ */
38
+ export function resizeObserver(target, options) {
39
+ const { callback, ...opts } = options
40
+ const observer = new ResizeObserver(observerFn)
41
+
42
+ if (target === window) target = document.body
43
+ useObserverMethodOnTarget(target, observer, 'observe', opts)
44
+
45
+ function observerFn(entries) {
46
+ for (const entry of entries) {
47
+ if (callback) callback({ entry, entries, observer })
48
+ else dispatchEvent(target, 'resize-obs', { entry, entries, observer })
49
+ }
50
+ }
51
+
52
+ return {
53
+ observe(target) {
54
+ useObserverMethodOnTarget(target, observer, 'observe', options)
55
+ },
56
+
57
+ unobserve(target) {
58
+ useObserverMethodOnTarget(target, observer, 'unobserve')
59
+ },
60
+
61
+ disconnect: _ => observer.disconnect(),
62
+ destroy: _ => observer.disconnect(),
63
+ }
64
+ }
@@ -0,0 +1,16 @@
1
+ import DOMPurify from 'dompurify'
2
+ import { sanitize as sanitizeLib } from '../lib/form/sanitize.js'
3
+
4
+ // Need to test if possible.
5
+ // We can't test browser on node. So, unless we do like playwright or some form of browser testing, we can't actually verify this works every time.
6
+ // Node version works already.
7
+ /**
8
+ * Sanitizes values using DOMPurify
9
+ * @param {*} value - Value to sanitize (string, array, or object)
10
+ * @param {Object} [options={}] - DOMPurify options
11
+ * @returns {*} Sanitized value
12
+ * @throws {Error} If input is a string but DOMPurify fails
13
+ */
14
+ export function sanitize(value, options = {}) {
15
+ return sanitizeLib(value, { sanitizer: DOMPurify.sanitize, ...options })
16
+ }
package/dom/trap-focus.js CHANGED
@@ -1,6 +1,12 @@
1
- import { Focusable } from './focusable.js'
1
+ import { getFocusableElements } from './focusable.js'
2
2
  import { isShiftTab, isTab } from './keyboard.js'
3
3
 
4
+ /**
5
+ * Traps focus within a set of focusable elements, creating a circular tab order
6
+ * @param {Object} params
7
+ * @param {KeyboardEvent} params.event - The keyboard event to handle
8
+ * @param {import('./focusable.js').Focusables} params.focusables - Object containing focusable elements
9
+ */
4
10
  export function trapFocus({ event, focusables }) {
5
11
  if (event.key !== 'Tab') return
6
12
  if (isTab(event) && document.activeElement === focusables.last) {
@@ -16,7 +22,16 @@ export function trapFocus({ event, focusables }) {
16
22
  }
17
23
  }
18
24
 
19
- /* Returns focus back to trigger node when user is done tabbing through the content */
25
+ /**
26
+ * Manages focus behavior when tabbing through content, with options to control focus on exit
27
+ * @param {Object} params
28
+ * @param {KeyboardEvent} params.event - The keyboard event to handle
29
+ * @param {HTMLElement} params.triggerNode - Element that triggered the focus trap
30
+ * @param {HTMLElement} params.contentNode - Container element that holds focusable content
31
+ * @param {import('./focusable.js').Focusables} params.focusables - Object containing focusable elements
32
+ * @param {'next'|'trigger'} [params.tabOutFocus='next'] - Where to focus when tabbing out: 'next' element or back to trigger
33
+ * @param {Function} [params.onLeave=()=>{}] - Callback when focus leaves content, receives 'forward' or 'back' direction
34
+ */
20
35
  export function manageFocus({
21
36
  event,
22
37
  triggerNode,
@@ -44,7 +59,7 @@ export function manageFocus({
44
59
  if (tabOutFocus === 'trigger') triggerNode.focus()
45
60
  if (tabOutFocus === 'next') {
46
61
  event.preventDefault()
47
- const DOMFocusables = Focusable().keyboard
62
+ const DOMFocusables = getFocusableElements().keyboard
48
63
  const index = DOMFocusables.findIndex(e => e === triggerNode)
49
64
  const next = DOMFocusables[index + 1]
50
65
  if (next) next.focus()
@@ -1,7 +1,19 @@
1
+ /**
2
+ * Fixes inconsistent button focus behavior across browsers.
3
+ *
4
+ * In some browsers (like Firefox), clicking a button doesn't focus it.
5
+ * This function ensures buttons are focused when clicked, making behavior consistent.
6
+ *
7
+ * @see {@link https://zellwk.com/blog/inconsistent-button-behavior/|Source}
8
+ * @example
9
+ * // Add the fix to your app
10
+ * inconsistentButtonFix()
11
+ */
1
12
  export function inconsistentButtonFix() {
2
13
  // https://zellwk.com/blog/inconsistent-button-behavior/
3
- document.addEventListener('click', function (event) {
4
- if (event.target.matches('button')) {
14
+ /** @param {MouseEvent} event */
15
+ document.addEventListener('click', event => {
16
+ if (event.target instanceof HTMLButtonElement) {
5
17
  event.target.focus()
6
18
  }
7
19
  })
@@ -1,10 +1,22 @@
1
- // Adjusted function to take excludes as an argument
2
- export function traverseAndScramble(node, { excludes } = {}) {
1
+ /**
2
+ * Traverses a DOM node and scrambles its text content, excluding specified elements.
3
+ * Useful for creating placeholder/loading states or privacy features.
4
+ *
5
+ * @param {Element|ChildNode} node - The root element or node to start traversal from
6
+ * @param {Object} [options] - Options for traversal
7
+ * @param {string[]} [options.excludes=[]] - Array of CSS selectors to exclude from scrambling
8
+ * @example
9
+ * // Scramble all text in a container
10
+ * traverseAndScramble(document.querySelector('.container'))
11
+ *
12
+ * // Scramble text but exclude certain elements
13
+ * traverseAndScramble(document.body, {
14
+ * excludes: ['.no-scramble', '[data-no-scramble]']
15
+ * })
16
+ */
17
+ export function traverseAndScramble(node, { excludes = [] } = {}) {
3
18
  // Check if the current node matches any of the exclude selectors
4
- if (
5
- excludes &&
6
- excludes.some(selector => node.matches && node.matches(selector))
7
- ) {
19
+ if (node instanceof Element && excludes.some(selector => node.matches(selector))) {
8
20
  return // Skip this node and its children
9
21
  }
10
22
 
@@ -12,14 +24,23 @@ export function traverseAndScramble(node, { excludes } = {}) {
12
24
  if (child.nodeType === 3) {
13
25
  // Node.TEXT_NODE
14
26
  child.nodeValue = scrambleText(child.nodeValue)
15
- } else if (child.nodeType === 1) {
27
+ } else if (child.nodeType === 1 && child instanceof Element) {
16
28
  // Node.ELEMENT_NODE
17
29
  traverseAndScramble(child, { excludes }) // Recurse into child elements with excludes
18
30
  }
19
31
  })
20
32
  }
21
33
 
22
- // Function to scramble text remains unchanged
34
+ /**
35
+ * Scrambles text by replacing alphanumeric characters with random ones.
36
+ * Preserves case, punctuation, and other symbols.
37
+ *
38
+ * @param {string} text - The text to scramble
39
+ * @returns {string} The scrambled text
40
+ * @private
41
+ * @example
42
+ * scrambleText('Hello123!') // Returns something like 'Xk9mP2q!'
43
+ */
23
44
  function scrambleText(text) {
24
45
  return text
25
46
  .split('')
@@ -0,0 +1,2 @@
1
+ import config from '@splendidlabz/eslint-config'
2
+ export default config
@@ -1,62 +1,5 @@
1
- export function last(array, index) {
2
- return index === array.length - 1
3
- }
4
-
5
- // Fisher Yates Shuffle without mutating the original array
6
- export function shuffle(array) {
7
- const clone = array.slice()
8
- let currentIndex = array.length
9
- let temporaryValue
10
- let randomIndex
11
-
12
- // While there remain elements to shuffle...
13
- while (currentIndex !== 0) {
14
- // Pick a remaining element...
15
- randomIndex = Math.floor(Math.random() * currentIndex)
16
- currentIndex -= 1
17
-
18
- // And swap it with the current element.
19
- temporaryValue = clone[currentIndex]
20
- clone[currentIndex] = clone[randomIndex]
21
- clone[randomIndex] = temporaryValue
22
- }
23
-
24
- return clone
25
- }
26
-
27
- /**
28
- * Sorts an array of objects or strings without mutating the array
29
- */
30
- export function sort(
31
- array,
32
- {
33
- property, // Property to sort by. Used when sorting array of objects.
34
- order = 'asc', // 'asc' or 'desc'
35
- },
36
- ) {
37
- const clone = array.slice()
38
- const sorted = clone.sort((a, b) => {
39
- let one = a
40
- let two = b
41
-
42
- // Use the `sortBy` property if sorting by objects
43
- if (property) {
44
- one = a[property]
45
- two = b[property]
46
- }
47
-
48
- if (order === 'asc') {
49
- if (one < two) return -1
50
- if (one > two) return 1
51
- }
52
-
53
- if (order === 'desc') {
54
- if (one < two) return 1
55
- if (one > two) return -1
56
- }
57
-
58
- return 0
59
- })
60
-
61
- return sorted
62
- }
1
+ export * from './item-position.js'
2
+ export * from './join.js'
3
+ export * from './shuffle.js'
4
+ export * from './sort.js'
5
+ export * from './unique.js'
@@ -0,0 +1,23 @@
1
+ export function last(array, index) {
2
+ console.warn('last is deprecated. Use isLastItem instead.')
3
+ return index === array.length - 1
4
+ }
5
+
6
+ /**
7
+ * Checks if the given index is the last item in the array
8
+ * @param {Array} array - The array to check
9
+ * @param {number} index - The index to check
10
+ * @returns {boolean} True if the index is the last item, false otherwise
11
+ */
12
+ export function isLastItem(array, index) {
13
+ return index === array.length - 1
14
+ }
15
+
16
+ /**
17
+ * Returns the last item in the array
18
+ * @param {Array} array - The array to get the last item from
19
+ * @returns {*} The last item in the array
20
+ */
21
+ export function lastArrayItem(array) {
22
+ return array[array.length - 1]
23
+ }
@@ -0,0 +1,20 @@
1
+ import { lastArrayItem } from './item-position.js'
2
+ /**
3
+ * Joins an array with a conjunction (and/or)
4
+ * @param {Array<string>} array - Array of strings to join
5
+ * @param {Object} [options] - Options for joining
6
+ * @param {string} [options.conjunction='and'] - Conjunction to use ('and' or 'or')
7
+ * @returns {string} Joined string
8
+ * @example
9
+ * joinWithConjunction(['a', 'b', 'c']) // 'a, b, and c'
10
+ * joinWithConjunction(['a', 'b'], { conjunction: 'or' }) // 'a or b'
11
+ */
12
+ export function joinWithConjunction(array, { conjunction = 'and' } = {}) {
13
+ if (!array?.length) return ''
14
+ if (array.length === 1) return array[0]
15
+ if (array.length === 2) return `${array[0]} ${conjunction} ${array[1]}`
16
+
17
+ const lastItem = lastArrayItem(array)
18
+ const rest = array.slice(0, -1)
19
+ return `${rest.join(', ')} ${conjunction} ${lastItem}`
20
+ }
@@ -0,0 +1,21 @@
1
+ // Fisher Yates Shuffle without mutating the original array
2
+ export function shuffle(array) {
3
+ const clone = array.slice()
4
+ let currentIndex = array.length
5
+ let temporaryValue
6
+ let randomIndex
7
+
8
+ // While there remain elements to shuffle...
9
+ while (currentIndex !== 0) {
10
+ // Pick a remaining element...
11
+ randomIndex = Math.floor(Math.random() * currentIndex)
12
+ currentIndex -= 1
13
+
14
+ // And swap it with the current element.
15
+ temporaryValue = clone[currentIndex]
16
+ clone[currentIndex] = clone[randomIndex]
17
+ clone[randomIndex] = temporaryValue
18
+ }
19
+
20
+ return clone
21
+ }
@@ -0,0 +1,88 @@
1
+ import { getNestedValue } from '../objects/nested-property.js'
2
+
3
+ /**
4
+ * Sorts an array of objects, numbers, or strings without mutating the original array.
5
+ * Supports nested properties, multiple sort keys, and various data types.
6
+ *
7
+ * @template T
8
+ * @param {T[]} array - Array to sort
9
+ * @param {Object} [options] - Sort options
10
+ * @param {string|string[]|null} [options.props=null] - Property or array of properties to sort by. If null, sorts simple arrays
11
+ * @param {boolean} [options.reverse=false] - Whether to sort in reverse order
12
+ * @returns {T[]} New sorted array
13
+ * @example
14
+ * // Sort simple array
15
+ * sort([3, 1, 2]) // [1, 2, 3]
16
+ *
17
+ * // Sort objects by single property
18
+ * sort(users, { props: 'name' })
19
+ *
20
+ * // Sort objects by multiple properties
21
+ * sort(users, { props: ['age', 'name'] })
22
+ *
23
+ * // Sort in reverse order
24
+ * sort(users, { props: 'age', reverse: true })
25
+ *
26
+ * // Sort by nested property
27
+ * sort(users, { props: 'address.city' })
28
+ */
29
+ export function sort(array, { props = null, reverse = false } = {}) {
30
+ const clone = array.slice()
31
+
32
+ // Handle simple arrays (numbers or strings)
33
+ if (props === null) {
34
+ return clone.sort((a, b) => {
35
+ const comparison = compareValues(a, b)
36
+ return reverse ? -comparison : comparison
37
+ })
38
+ }
39
+
40
+ // Handle objects in arrays
41
+ const properties = Array.isArray(props) ? props : [props]
42
+
43
+ return clone.sort((a, b) => {
44
+ for (const property of properties) {
45
+ const aValue = getNestedValue(a, property)
46
+ const bValue = getNestedValue(b, property)
47
+
48
+ // Skip if values are equal
49
+ if (aValue === bValue) continue
50
+
51
+ // Push null/undefined values to the end
52
+ if (aValue == null) return 1
53
+ if (bValue == null) return -1
54
+
55
+ // Compare values and handle reverse sort
56
+ const comparison = compareValues(aValue, bValue)
57
+ return reverse ? -comparison : comparison
58
+ }
59
+ return 0
60
+ })
61
+ }
62
+
63
+ /**
64
+ * Compares two values for sorting, handling different data types.
65
+ * Supports numbers, strings, dates, and falls back to string comparison.
66
+ *
67
+ * @param {*} a - First value to compare
68
+ * @param {*} b - Second value to compare
69
+ * @returns {number} -1 if a < b, 0 if a === b, 1 if a > b
70
+ * @private
71
+ */
72
+ function compareValues(a, b) {
73
+ // Compare numbers
74
+ if (typeof a === 'number' && typeof b === 'number') return a - b
75
+
76
+ // Compare strings
77
+ if (typeof a === 'string' && typeof b === 'string') return a.localeCompare(b)
78
+
79
+ // Compare dates
80
+ const aDate = new Date(a)
81
+ const bDate = new Date(b)
82
+ const aTime = aDate.getTime()
83
+ const bTime = bDate.getTime()
84
+ if (!isNaN(aTime) && !isNaN(bTime)) return aTime - bTime
85
+
86
+ // Convert into strings and compare
87
+ return String(a).localeCompare(String(b))
88
+ }