@splendidlabz/utils 1.3.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 (93) hide show
  1. package/.eslintrc.cjs +3 -0
  2. package/.turbo/turbo-lint.log +10 -0
  3. package/.turbo/turbo-test.log +10 -0
  4. package/CHANGELOG.md +37 -0
  5. package/actions/index.js +5 -0
  6. package/actions/masonry.js +38 -0
  7. package/actions/mutation-observer.js +43 -0
  8. package/actions/prefer-horizontal-scroll.js +125 -0
  9. package/actions/resize-observer.js +38 -0
  10. package/actions/sticky.js +88 -0
  11. package/dom/bounding-box.js +54 -0
  12. package/dom/clipboard.js +14 -0
  13. package/dom/cookie.js +31 -0
  14. package/dom/css-vars.js +11 -0
  15. package/dom/events.js +76 -0
  16. package/dom/events.test.js +99 -0
  17. package/dom/focusable.js +56 -0
  18. package/dom/font-size.js +24 -0
  19. package/dom/get-element.js +69 -0
  20. package/dom/hash.js +9 -0
  21. package/dom/index.js +18 -0
  22. package/dom/keyboard.js +39 -0
  23. package/dom/local-store.js +29 -0
  24. package/dom/media.js +27 -0
  25. package/dom/pkce.js +34 -0
  26. package/dom/query-params.js +14 -0
  27. package/dom/random-string.js +18 -0
  28. package/dom/session-store.js +29 -0
  29. package/dom/trap-focus.js +55 -0
  30. package/dom/ui/aria-current.js +20 -0
  31. package/dom/ui/inconsistent-button-fix.js +8 -0
  32. package/dom/ui/index.js +4 -0
  33. package/dom/ui/scroll-container.js +25 -0
  34. package/dom/ui/traverse-and-scramble.js +40 -0
  35. package/lib/arrays/index.js +62 -0
  36. package/lib/auth/index.js +1 -0
  37. package/lib/auth/route-manager.js +44 -0
  38. package/lib/checks.js +11 -0
  39. package/lib/date/days.js +9 -0
  40. package/lib/date/index.js +2 -0
  41. package/lib/date/months.js +74 -0
  42. package/lib/form/form-data.js +73 -0
  43. package/lib/form/index.js +2 -0
  44. package/lib/form/sanitize.js +47 -0
  45. package/lib/functions/debounce.js +18 -0
  46. package/lib/functions/env.js +5 -0
  47. package/lib/functions/functional.js +31 -0
  48. package/lib/functions/index.js +5 -0
  49. package/lib/functions/throttle.js +11 -0
  50. package/lib/functions/timeout.js +15 -0
  51. package/lib/index.js +12 -0
  52. package/lib/index.test.js +9 -0
  53. package/lib/numbers/index.js +10 -0
  54. package/lib/objects/camelcase-keys.js +17 -0
  55. package/lib/objects/camelcase-keys.test.js +72 -0
  56. package/lib/objects/empty.js +10 -0
  57. package/lib/objects/extend.js +16 -0
  58. package/lib/objects/extend.test.js +35 -0
  59. package/lib/objects/flatten.js +19 -0
  60. package/lib/objects/index.js +12 -0
  61. package/lib/objects/json.js +7 -0
  62. package/lib/objects/loop.js +11 -0
  63. package/lib/objects/loop.test.js +31 -0
  64. package/lib/objects/mix/mix.js +100 -0
  65. package/lib/objects/mix/mix.md +78 -0
  66. package/lib/objects/mix/mix.test.js +324 -0
  67. package/lib/objects/nested-property.js +36 -0
  68. package/lib/objects/normalize-object.js +10 -0
  69. package/lib/objects/omit-empty.js +23 -0
  70. package/lib/objects/omit-empty.test.js +85 -0
  71. package/lib/objects/size.js +41 -0
  72. package/lib/objects/split.js +19 -0
  73. package/lib/promises/index.js +1 -0
  74. package/lib/promises/reject.js +17 -0
  75. package/lib/strings/convert-case/convert-case.js +86 -0
  76. package/lib/strings/convert-case/convert-case.md +44 -0
  77. package/lib/strings/convert-case/convert-case.test.js +50 -0
  78. package/lib/strings/index.js +4 -0
  79. package/lib/strings/markdown.js +39 -0
  80. package/lib/strings/pluralize.js +2 -0
  81. package/lib/strings/query-string.js +18 -0
  82. package/lib/style/index.js +11 -0
  83. package/lib/symbols/index.js +1 -0
  84. package/lib/symbols/symbols.js +9 -0
  85. package/node/common.js +5 -0
  86. package/node/dirname.js +17 -0
  87. package/node/file-cache.js +135 -0
  88. package/node/file.js +13 -0
  89. package/node/hash.js +5 -0
  90. package/node/index.js +6 -0
  91. package/node/pkce.js +33 -0
  92. package/node/random-string.js +10 -0
  93. package/package.json +35 -0
package/.eslintrc.cjs ADDED
@@ -0,0 +1,3 @@
1
+ module.exports = {
2
+ extends: ['@splendidlabz/eslint-config'],
3
+ }
@@ -0,0 +1,10 @@
1
+
2
+ > @splendidlabs/utils@1.0.3 lint
3
+ > eslint . --fix
4
+
5
+ 
6
+ /Users/zellwk/projects/@splendidlabs/packages/utils/lib/index.test.js
7
+  4:10 error 'expect' is defined but never used no-unused-vars
8
+ 
9
+ ✖ 1 problem (1 error, 0 warnings)
10
+ 
@@ -0,0 +1,10 @@
1
+
2
+ > @splendidlabs/utils@1.0.3 test
3
+ > vitest run
4
+
5
+ sh: vitest: command not found
6
+ npm ERR! Lifecycle script `test` failed with error:
7
+ npm ERR! Error: command failed
8
+ npm ERR!  in workspace: @splendidlabs/utils@1.0.3
9
+ npm ERR!  at location: /Users/zellwk/projects/@splendidlabs/packages/utils
10
+ 
package/CHANGELOG.md ADDED
@@ -0,0 +1,37 @@
1
+ # @splendidlabz/utils
2
+
3
+ ## 1.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - a9128f4: Add Debounce and Throttle functions
8
+
9
+ ## 1.2.0
10
+
11
+ ### Minor Changes
12
+
13
+ - a9128f4: - Add `uuid` to `utils/node`
14
+ - Add `uuid` to `utils/dom`
15
+ - Add `MONTHS` constant to `utils`
16
+
17
+ ### Patch Changes
18
+
19
+ - a9128f4: - Change `randomString` in `utils/dom` to produce a HTML-usable string
20
+
21
+ ## 1.1.2
22
+
23
+ ### Patch Changes
24
+
25
+ - Add query params path
26
+
27
+ ## 1.1.1
28
+
29
+ ### Patch Changes
30
+
31
+ - Fix session store path
32
+
33
+ ## 1.1.0
34
+
35
+ ### Minor Changes
36
+
37
+ - 2d5789f: New and better cache utility
@@ -0,0 +1,5 @@
1
+ export * from './masonry.js'
2
+ export * from './mutation-observer.js'
3
+ export * from './prefer-horizontal-scroll.js'
4
+ export * from './resize-observer.js'
5
+ export * from './sticky.js'
@@ -0,0 +1,38 @@
1
+ import { getChildrenElements, mediaLoaded } from '../dom/index.js'
2
+ import { resizeObserver } from './resize-observer.js'
3
+
4
+ export async function masonry(node) {
5
+ if (nativeMasonrySupport(node)) return
6
+
7
+ let items
8
+ let gap
9
+
10
+ node.style.setProperty('row-gap', '1px', 'important')
11
+ node.style.gridAutoRows = '0px'
12
+ await mediaLoaded(node)
13
+
14
+ const obs = resizeObserver(node, {
15
+ async callback(opts) {
16
+ items = getChildrenElements(node)
17
+ gap = parseFloat(getComputedStyle(node).columnGap)
18
+ layout(node)
19
+ },
20
+ })
21
+
22
+ async function layout() {
23
+ items.forEach(item => {
24
+ const itemBox = item.getBoundingClientRect()
25
+ item.style.gridRowEnd = `span ${Math.round(itemBox.height + gap)}`
26
+ })
27
+ }
28
+
29
+ return {
30
+ destroy() {
31
+ obs.disconnect()
32
+ },
33
+ }
34
+ }
35
+
36
+ function nativeMasonrySupport(node) {
37
+ return getComputedStyle(node).gridTemplateRows === 'masonry'
38
+ }
@@ -0,0 +1,43 @@
1
+ /* eslint-env browser */
2
+
3
+ // https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/observe
4
+
5
+ // These are simply options that will often be used. Check the docs for other options.
6
+ const defaultOptions = {
7
+ attributes: false,
8
+ childList: false,
9
+ subtree: false,
10
+ }
11
+
12
+ export function mutationObserver(node, options) {
13
+ options = { ...defaultOptions, ...options }
14
+ const { callback, ...opts } = options
15
+ const observer = new MutationObserver(observerFn)
16
+
17
+ function observerFn(entries) {
18
+ for (const entry of entries) {
19
+ if (callback) callback({ node, entry, observer })
20
+ else {
21
+ node.dispatchEvent(
22
+ new CustomEvent('mutate', {
23
+ detail: { node, entry, observer },
24
+ }),
25
+ )
26
+ }
27
+ }
28
+ }
29
+
30
+ if (node === window) node = document.body
31
+ observer.observe(node, opts)
32
+
33
+ return {
34
+ disconnect() {
35
+ observer.disconnect()
36
+ },
37
+
38
+ // Destroy is used for Svelte actions
39
+ destroy() {
40
+ observer.disconnect()
41
+ },
42
+ }
43
+ }
@@ -0,0 +1,125 @@
1
+ import {
2
+ addListeners,
3
+ getCSSVar,
4
+ removeListeners,
5
+ setCSSVar,
6
+ } from '../dom/index.js'
7
+ import { omitEmpty } from '../lib/index.js'
8
+
9
+ const DEFAULT_OPTIONS = {
10
+ scrollSnapDelay: 1000,
11
+ }
12
+
13
+ export function preferHorizontalScroll(node, props = {}) {
14
+ if (!node.classList.contains('Scrollable-preferHorizontalScroll')) return
15
+
16
+ const state = {
17
+ origSnapType: null,
18
+ currentSnapType: null,
19
+ snapTimeoutID: null,
20
+ }
21
+
22
+ const options = {
23
+ ...DEFAULT_OPTIONS,
24
+ ...omitEmpty({
25
+ scrollSnapDelay: getCSSVar(node, '--scrollSnapDelay'),
26
+ }),
27
+ ...omitEmpty(props),
28
+ }
29
+
30
+ const listeners = [
31
+ {
32
+ event: 'wheel',
33
+ node,
34
+ handler: handleWheel,
35
+ },
36
+
37
+ {
38
+ event: 'pointerenter',
39
+ node,
40
+ handler: handlePointerEnter,
41
+ },
42
+
43
+ {
44
+ event: 'pointerleave',
45
+ node,
46
+ handler: handlePointerLeave,
47
+ },
48
+ ]
49
+
50
+ addListeners(listeners)
51
+
52
+ function handleWheel(event) {
53
+ const direction = event.deltaY > 0 ? 'down' : 'up'
54
+ const updatedDelta = event.deltaY
55
+
56
+ // Only handle mouse wheel
57
+ // Trackpad users can scroll horizontally themselves
58
+ if (isTrackpad(event)) return false
59
+
60
+ setCSSVar(node, '--snap-type', 'none')
61
+
62
+ const scrolledAmount = node.scrollLeft
63
+ const scrollWidth = node.scrollWidth
64
+ const clientWidth = node.clientWidth
65
+ const scrollableAmount = scrollWidth - clientWidth // Maximum scrollable width to the right
66
+
67
+ // Hijack downwards scroll
68
+ if (direction === 'down' && scrolledAmount < scrollableAmount) {
69
+ node.scrollLeft += updatedDelta
70
+ event.preventDefault()
71
+ }
72
+
73
+ // Hijack upwards scroll
74
+ if (direction === 'up' && scrolledAmount > 0) {
75
+ node.scrollLeft += updatedDelta
76
+ event.preventDefault()
77
+ }
78
+
79
+ // Restore snap type after a delay
80
+ const { snapTimeoutID } = state
81
+ if (snapTimeoutID) clearTimeout(snapTimeoutID)
82
+ state.snapTimeoutID = setTimeout(_ => {
83
+ restoreHijackedState(node, state)
84
+ }, options.scrollSnapDelay)
85
+ }
86
+
87
+ function handlePointerEnter(event) {
88
+ state.origSnapType = getCSSVar(node, '--snap-type') || 'proximity'
89
+ }
90
+
91
+ function handlePointerLeave(event) {
92
+ restoreHijackedState(node, state)
93
+ }
94
+
95
+ return {
96
+ destroy() {
97
+ removeListeners(listeners)
98
+ },
99
+ }
100
+ }
101
+
102
+ function restoreHijackedState(node, state) {
103
+ const scrollLeft = node.scrollLeft
104
+
105
+ // Get the final scroll position
106
+ setCSSVar(node, '--snap-type', state.origSnapType)
107
+ const finalScrollLeft = node.scrollLeft
108
+
109
+ // Scroll back to original position before replacing snap
110
+ setCSSVar(node, '--snap-type', 'none')
111
+ node.scrollTo({ left: scrollLeft, behavior: 'instant' })
112
+
113
+ // Scroll back to the final position smoothly
114
+ node.scrollTo({ left: finalScrollLeft, behavior: 'smooth' })
115
+
116
+ setTimeout(_ => {
117
+ setCSSVar(node, '--snap-type', state.origSnapType)
118
+ }, 250)
119
+ }
120
+
121
+ // https://stackoverflow.com/questions/10744645/detect-touchpad-vs-mouse-in-javascript/62415754#62415754
122
+ function isTrackpad(event) {
123
+ // if (event.deltaMode === 0) return true // Deltamode no longer works in Chrome
124
+ if (event.wheelDeltaY === event.deltaY * -3) return true
125
+ }
@@ -0,0 +1,38 @@
1
+ /* eslint-env browser */
2
+
3
+ const DEFAULT_OBSERVER_OPTIONS = {
4
+ observe: true,
5
+ }
6
+
7
+ export function resizeObserver(node, options) {
8
+ // We set the option here to prevent observer from being created unless necessary.
9
+ options = { ...DEFAULT_OBSERVER_OPTIONS, ...options }
10
+ if (!options.observe) return
11
+
12
+ const observer = new ResizeObserver(observerFn)
13
+
14
+ function observerFn(entries) {
15
+ for (const entry of entries) {
16
+ if (options.callback) options.callback({ node, entry, observer })
17
+ else {
18
+ node.dispatchEvent(
19
+ new CustomEvent('resize', {
20
+ detail: { node, entry, observer },
21
+ }),
22
+ )
23
+ }
24
+ }
25
+ }
26
+
27
+ observer.observe(node)
28
+
29
+ return {
30
+ disconnect() {
31
+ observer.unobserve(node)
32
+ },
33
+
34
+ destroy() {
35
+ observer.unobserve(node)
36
+ },
37
+ }
38
+ }
@@ -0,0 +1,88 @@
1
+ import {
2
+ boundingBoxRelativeToAncestor,
3
+ findScrollContainer,
4
+ } from '../dom/index.js'
5
+ import { resizeObserver } from './resize-observer.js'
6
+
7
+ export function sticky(node, props = {}) {
8
+ const scrollContainer = findScrollContainer(node)
9
+
10
+ let stickyTBLR = getComputedTBLR(node)
11
+
12
+ const obs = resizeObserver(node, {
13
+ callback: ({ entry }) => {
14
+ stickyTBLR = getComputedTBLR(node)
15
+ isSticking()
16
+
17
+ // Arguably, one can use a single container to check for all Sticky elements, but that is unnecessarily complicated to code. This adds more listeners to the scroll containers (assuming many), but it shouldn't be have enough listeners to break performance in any way.
18
+ scrollContainer === document.documentElement
19
+ ? window.addEventListener('scroll', scrollEventListener)
20
+ : scrollContainer.addEventListener('scroll', scrollEventListener)
21
+ },
22
+ })
23
+
24
+ function scrollEventListener(event) {
25
+ isSticking()
26
+ }
27
+
28
+ function isSticking() {
29
+ const { top, bottom, left, right } = stickyTBLR
30
+ const relativeRect = boundingBoxRelativeToAncestor({
31
+ element: node,
32
+ ancestorElement: scrollContainer,
33
+ })
34
+
35
+ // Sticky Top
36
+ if (!isNaN(top)) {
37
+ if (relativeRect.top - top <= 1) {
38
+ node.classList.add('sticking')
39
+ } else {
40
+ node.classList.remove('sticking')
41
+ }
42
+ }
43
+
44
+ // Sticky Bottom
45
+ if (!isNaN(bottom)) {
46
+ if (relativeRect.bottom + bottom >= -1) {
47
+ node.classList.add('sticking')
48
+ } else {
49
+ node.classList.remove('sticking')
50
+ }
51
+ }
52
+
53
+ // Sticky Left
54
+ if (!isNaN(left)) {
55
+ if (relativeRect.left - left <= 1) {
56
+ node.classList.add('sticking')
57
+ } else {
58
+ node.classList.remove('sticking')
59
+ }
60
+ }
61
+
62
+ // Sticky Right
63
+ if (!isNaN(right)) {
64
+ if (relativeRect.right + right >= -1) {
65
+ node.classList.add('sticking')
66
+ } else {
67
+ node.classList.remove('sticking')
68
+ }
69
+ }
70
+ }
71
+
72
+ return {
73
+ destroy() {
74
+ obs.disconnect()
75
+ scrollContainer.removeEventListener('scroll', scrollEventListener)
76
+ },
77
+ }
78
+ }
79
+
80
+ function getComputedTBLR(element) {
81
+ const style = getComputedStyle(element)
82
+ return {
83
+ top: parseFloat(style.top),
84
+ bottom: parseFloat(style.bottom),
85
+ left: parseFloat(style.left),
86
+ right: parseFloat(style.right),
87
+ }
88
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Advanced boundincClientRect that contains information vertical and horizontal centers.
3
+ * @param {HTMLElement} element
4
+ */
5
+ export function boundingBox(element, { containsPadding = true } = {}) {
6
+ const box = element.getBoundingClientRect()
7
+
8
+ // Spread original properties into the return object
9
+ const ret = {}
10
+ for (const prop in box) ret[prop] = box[prop]
11
+
12
+ // Gets boundingClientRect value without padding
13
+ if (!containsPadding) {
14
+ const style = getComputedStyle(element)
15
+ let { paddingTop, paddingRight, paddingBottom, paddingLeft } = style
16
+ paddingTop = parseFloat(paddingTop)
17
+ paddingRight = parseFloat(paddingRight)
18
+ paddingBottom = parseFloat(paddingBottom)
19
+ paddingLeft = parseFloat(paddingLeft)
20
+
21
+ ret.x = ret.x + paddingLeft
22
+ ret.y = ret.y + paddingTop
23
+ ret.width = ret.width - paddingLeft - paddingRight
24
+ ret.height = ret.height - paddingTop - paddingBottom
25
+ ret.top = ret.top + paddingTop
26
+ ret.bottom = ret.bottom - paddingBottom
27
+ ret.right = ret.right - paddingRight
28
+ ret.left = ret.left + paddingLeft
29
+ }
30
+
31
+ ret.xCenter = (ret.left + ret.right) / 2
32
+ ret.yCenter = (ret.top + ret.bottom) / 2
33
+ return ret
34
+ }
35
+
36
+ export function boundingBoxRelativeToAncestor({ element, ancestorElement }) {
37
+ ancestorElement = ancestorElement || element.parentElement
38
+
39
+ const box = boundingBox(element)
40
+ const ancestorBox = boundingBox(ancestorElement, {
41
+ containsPadding: false,
42
+ })
43
+
44
+ return {
45
+ top: box.top - ancestorBox.top,
46
+ right: box.right - ancestorBox.right,
47
+ bottom: box.bottom - ancestorBox.bottom,
48
+ left: box.left - ancestorBox.left,
49
+ width: box.width,
50
+ height: box.height,
51
+ xCenter: (box.left + box.right) / 2 - ancestorBox.left,
52
+ yCenter: (box.top + box.bottom) / 2 - ancestorBox.top,
53
+ }
54
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Copies rich text from an element to the clipboard
3
+ * @param HTMLElement element - Element to copy
4
+ */
5
+ export function copyRichText(element) {
6
+ const range = document.createRange()
7
+ range.selectNode(element)
8
+
9
+ const selection = window.getSelection()
10
+ selection.removeAllRanges()
11
+ selection.addRange(range)
12
+ document.execCommand('copy')
13
+ selection.removeAllRanges()
14
+ }
package/dom/cookie.js ADDED
@@ -0,0 +1,31 @@
1
+ export const cookies = {
2
+ get(name) {
3
+ return getCookie(name)
4
+ },
5
+
6
+ set(name, value, days) {
7
+ setCookie(name, value, days)
8
+ },
9
+ }
10
+
11
+ /**
12
+ * Gets a cookie by name
13
+ * Thanks to https://stackoverflow.com/a/21125098
14
+ * @param {string} name Cookie name
15
+ * @returns string Cookie value
16
+ */
17
+ export function getCookie(name) {
18
+ const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'))
19
+ if (match) return match[2]
20
+ }
21
+
22
+ // By Copilot. Needs testing before confirmation.
23
+ function setCookie(name, value, days) {
24
+ let expires = ''
25
+ if (days) {
26
+ const date = new Date()
27
+ date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000)
28
+ expires = '; expires=' + date.toUTCString()
29
+ }
30
+ document.cookie = name + '=' + (value || '') + expires + '; path=/'
31
+ }
@@ -0,0 +1,11 @@
1
+ import { getElement } from './get-element.js'
2
+
3
+ export function getCSSVar(element, prop) {
4
+ element = getElement(element)
5
+ return getComputedStyle(element).getPropertyValue(prop)
6
+ }
7
+
8
+ export function setCSSVar(element, prop, value) {
9
+ element = getElement(element)
10
+ element.style.setProperty(prop, value)
11
+ }
package/dom/events.js ADDED
@@ -0,0 +1,76 @@
1
+ export function addListeners(listeners) {
2
+ listeners.forEach(({ node, event, handler }) => {
3
+ node.addEventListener(event, handler)
4
+ })
5
+ }
6
+
7
+ export function removeListeners(listeners) {
8
+ listeners.forEach(({ node, event, handler }) => {
9
+ node.removeEventListener(event, handler)
10
+ })
11
+ }
12
+
13
+ export function dispatchEvent(node, eventName, detail, options = {}) {
14
+ node.dispatchEvent(
15
+ new CustomEvent(eventName, {
16
+ ...options,
17
+ detail,
18
+ }),
19
+ )
20
+ }
21
+
22
+ // Updates an event with new properties
23
+ export function updateEvent(event, updates) {
24
+ const { type, ...rest } = event
25
+ return new CustomEvent(event.type, {
26
+ ...rest,
27
+ ...updates,
28
+ })
29
+ }
30
+
31
+ // Doesn't seem to be used anywhere now.
32
+ // Switch to addListeners and removeListeners instead
33
+ export function createListeners(listeners) {
34
+ listeners = listeners || []
35
+
36
+ listeners.forEach(listener => {
37
+ const { node, event, handler } = listener
38
+ node.addEventListener(event, handler)
39
+ })
40
+
41
+ return {
42
+ get list() {
43
+ return listeners
44
+ },
45
+
46
+ add({ node, event, handler }) {
47
+ listeners.push({ node, event, handler })
48
+ node.addEventListener(event, handler)
49
+ },
50
+
51
+ remove({ node, event, handler }) {
52
+ const index = listeners.findIndex(listener => {
53
+ if (
54
+ listener.node === node &&
55
+ listener.event === event &&
56
+ listener.handler === handler
57
+ ) {
58
+ return true
59
+ }
60
+ return false
61
+ })
62
+
63
+ if (index !== -1) {
64
+ listeners.splice(index, 1)
65
+ node.removeEventListener(event, handler)
66
+ }
67
+ },
68
+
69
+ clear() {
70
+ listeners.forEach(({ node, event, handler }) => {
71
+ listeners = []
72
+ node.removeEventListener(event, handler)
73
+ })
74
+ },
75
+ }
76
+ }
@@ -0,0 +1,99 @@
1
+ // @vitest-environment jsdom
2
+ import { describe, expect, it } from 'vitest'
3
+
4
+ import { createListeners } from './events.js'
5
+
6
+ describe('Listeners Factory', _ => {
7
+ it('Empty', _ => {
8
+ const listeners = createListeners()
9
+ expect(listeners.list).toEqual([])
10
+ })
11
+
12
+ it('Begins with some listeners', _ => {
13
+ const node1 = document.createElement('div')
14
+ const node2 = document.createElement('p')
15
+ const event1 = 'click'
16
+ const event2 = 'keydown'
17
+ const cb1 = _ => {}
18
+ const cb2 = _ => {}
19
+
20
+ const listeners = createListeners([
21
+ { node: node1, event: event1, handler: cb1 },
22
+ { node: node2, event: event2, handler: cb2 },
23
+ ])
24
+
25
+ expect(listeners.list).toEqual([
26
+ { node: node1, event: event1, handler: cb1 },
27
+ { node: node2, event: event2, handler: cb2 },
28
+ ])
29
+ })
30
+
31
+ it('Add', _ => {
32
+ const node = document.createElement('div')
33
+ const event = 'click'
34
+ const handler = _ => {
35
+ node.setAttribute('clicked', true)
36
+ }
37
+
38
+ const listeners = createListeners()
39
+ listeners.add({ node, event, handler })
40
+
41
+ // Ensures the listener was added to the list
42
+ expect(listeners.list.length).toBe(1)
43
+
44
+ // Ensures the listener works
45
+ node.click()
46
+ expect(node.getAttribute('clicked')).toBe('true')
47
+ })
48
+
49
+ it('Remove', _ => {
50
+ const node = document.createElement('div')
51
+ const event = 'click'
52
+ const handler = _ => {
53
+ const count = node.getAttribute('count') || 0
54
+ node.setAttribute('count', count + 1)
55
+ }
56
+
57
+ const listener = { node, event, handler }
58
+ const listeners = createListeners([listener])
59
+
60
+ node.click()
61
+ expect(node.getAttribute('count')).toBe('1')
62
+
63
+ listeners.remove(listener)
64
+
65
+ // Ensures listener has been removed
66
+ node.click()
67
+
68
+ expect(listeners.list.length).toBe(0)
69
+ expect(node.getAttribute('count')).toBe('1')
70
+ })
71
+
72
+ it('Clears all listeners from', _ => {
73
+ const node1 = document.createElement('div')
74
+ const node2 = document.createElement('p')
75
+ const event1 = 'click'
76
+ const event2 = 'click'
77
+ const cb1 = _ => {
78
+ node1.setAttribute('clicked', true)
79
+ }
80
+ const cb2 = _ => {
81
+ node2.setAttribute('clicked', true)
82
+ }
83
+
84
+ const listeners = createListeners([
85
+ { node: node1, event: event1, handler: cb1 },
86
+ { node: node2, event: event2, handler: cb2 },
87
+ ])
88
+
89
+ listeners.clear()
90
+ expect(listeners.list).toEqual([])
91
+
92
+ // Ensures none of the listeners work anymore
93
+ node1.click()
94
+ node2.click()
95
+
96
+ expect(node1.getAttribute('clicked')).toBe(null)
97
+ expect(node2.getAttribute('clicked')).toBe(null)
98
+ })
99
+ })