@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.
- package/.eslintrc.cjs +3 -0
- package/.turbo/turbo-lint.log +10 -0
- package/.turbo/turbo-test.log +10 -0
- package/CHANGELOG.md +37 -0
- package/actions/index.js +5 -0
- package/actions/masonry.js +38 -0
- package/actions/mutation-observer.js +43 -0
- package/actions/prefer-horizontal-scroll.js +125 -0
- package/actions/resize-observer.js +38 -0
- package/actions/sticky.js +88 -0
- package/dom/bounding-box.js +54 -0
- package/dom/clipboard.js +14 -0
- package/dom/cookie.js +31 -0
- package/dom/css-vars.js +11 -0
- package/dom/events.js +76 -0
- package/dom/events.test.js +99 -0
- package/dom/focusable.js +56 -0
- package/dom/font-size.js +24 -0
- package/dom/get-element.js +69 -0
- package/dom/hash.js +9 -0
- package/dom/index.js +18 -0
- package/dom/keyboard.js +39 -0
- package/dom/local-store.js +29 -0
- package/dom/media.js +27 -0
- package/dom/pkce.js +34 -0
- package/dom/query-params.js +14 -0
- package/dom/random-string.js +18 -0
- package/dom/session-store.js +29 -0
- package/dom/trap-focus.js +55 -0
- package/dom/ui/aria-current.js +20 -0
- package/dom/ui/inconsistent-button-fix.js +8 -0
- package/dom/ui/index.js +4 -0
- package/dom/ui/scroll-container.js +25 -0
- package/dom/ui/traverse-and-scramble.js +40 -0
- package/lib/arrays/index.js +62 -0
- package/lib/auth/index.js +1 -0
- package/lib/auth/route-manager.js +44 -0
- package/lib/checks.js +11 -0
- package/lib/date/days.js +9 -0
- package/lib/date/index.js +2 -0
- package/lib/date/months.js +74 -0
- package/lib/form/form-data.js +73 -0
- package/lib/form/index.js +2 -0
- package/lib/form/sanitize.js +47 -0
- package/lib/functions/debounce.js +18 -0
- package/lib/functions/env.js +5 -0
- package/lib/functions/functional.js +31 -0
- package/lib/functions/index.js +5 -0
- package/lib/functions/throttle.js +11 -0
- package/lib/functions/timeout.js +15 -0
- package/lib/index.js +12 -0
- package/lib/index.test.js +9 -0
- package/lib/numbers/index.js +10 -0
- package/lib/objects/camelcase-keys.js +17 -0
- package/lib/objects/camelcase-keys.test.js +72 -0
- package/lib/objects/empty.js +10 -0
- package/lib/objects/extend.js +16 -0
- package/lib/objects/extend.test.js +35 -0
- package/lib/objects/flatten.js +19 -0
- package/lib/objects/index.js +12 -0
- package/lib/objects/json.js +7 -0
- package/lib/objects/loop.js +11 -0
- package/lib/objects/loop.test.js +31 -0
- package/lib/objects/mix/mix.js +100 -0
- package/lib/objects/mix/mix.md +78 -0
- package/lib/objects/mix/mix.test.js +324 -0
- package/lib/objects/nested-property.js +36 -0
- package/lib/objects/normalize-object.js +10 -0
- package/lib/objects/omit-empty.js +23 -0
- package/lib/objects/omit-empty.test.js +85 -0
- package/lib/objects/size.js +41 -0
- package/lib/objects/split.js +19 -0
- package/lib/promises/index.js +1 -0
- package/lib/promises/reject.js +17 -0
- package/lib/strings/convert-case/convert-case.js +86 -0
- package/lib/strings/convert-case/convert-case.md +44 -0
- package/lib/strings/convert-case/convert-case.test.js +50 -0
- package/lib/strings/index.js +4 -0
- package/lib/strings/markdown.js +39 -0
- package/lib/strings/pluralize.js +2 -0
- package/lib/strings/query-string.js +18 -0
- package/lib/style/index.js +11 -0
- package/lib/symbols/index.js +1 -0
- package/lib/symbols/symbols.js +9 -0
- package/node/common.js +5 -0
- package/node/dirname.js +17 -0
- package/node/file-cache.js +135 -0
- package/node/file.js +13 -0
- package/node/hash.js +5 -0
- package/node/index.js +6 -0
- package/node/pkce.js +33 -0
- package/node/random-string.js +10 -0
- package/package.json +35 -0
package/.eslintrc.cjs
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
|
|
2
|
+
> @splendidlabs/utils@1.0.3 lint
|
|
3
|
+
> eslint . --fix
|
|
4
|
+
|
|
5
|
+
[0m[0m
|
|
6
|
+
[0m[4m/Users/zellwk/projects/@splendidlabs/packages/utils/lib/index.test.js[24m[0m
|
|
7
|
+
[0m [2m4:10[22m [31merror[39m 'expect' is defined but never used [2mno-unused-vars[22m[0m
|
|
8
|
+
[0m[0m
|
|
9
|
+
[0m[31m[1m✖ 1 problem (1 error, 0 warnings)[22m[39m[0m
|
|
10
|
+
[0m[31m[1m[22m[39m[0m
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
|
|
2
|
+
> @splendidlabs/utils@1.0.3 test
|
|
3
|
+
> vitest run
|
|
4
|
+
|
|
5
|
+
sh: vitest: command not found
|
|
6
|
+
[37;40mnpm[0m [0m[31;40mERR![0m [0m[35mLifecycle script `test` failed with error:[0m
|
|
7
|
+
[0m[37;40mnpm[0m [0m[31;40mERR![0m [0m[35mError: command failed[0m
|
|
8
|
+
[0m[37;40mnpm[0m [0m[31;40mERR![0m [0m[35m in workspace: @splendidlabs/utils@1.0.3[0m
|
|
9
|
+
[0m[37;40mnpm[0m [0m[31;40mERR![0m [0m[35m at location: /Users/zellwk/projects/@splendidlabs/packages/utils[0m
|
|
10
|
+
[0m
|
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
|
package/actions/index.js
ADDED
|
@@ -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
|
+
}
|
package/dom/clipboard.js
ADDED
|
@@ -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
|
+
}
|
package/dom/css-vars.js
ADDED
|
@@ -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
|
+
})
|