@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/dom/focusable.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export function Focusable(element = document) {
|
|
2
|
+
return {
|
|
3
|
+
get length() {
|
|
4
|
+
return this.keyboard.length
|
|
5
|
+
},
|
|
6
|
+
|
|
7
|
+
get all() {
|
|
8
|
+
return [
|
|
9
|
+
...element.querySelectorAll(
|
|
10
|
+
`a,
|
|
11
|
+
button,
|
|
12
|
+
input,
|
|
13
|
+
textarea,
|
|
14
|
+
select,
|
|
15
|
+
details,
|
|
16
|
+
iframe,
|
|
17
|
+
embed,
|
|
18
|
+
object,
|
|
19
|
+
summary,
|
|
20
|
+
dialog,
|
|
21
|
+
audio[controls],
|
|
22
|
+
video[controls],
|
|
23
|
+
[contenteditable],
|
|
24
|
+
[tabindex]
|
|
25
|
+
`
|
|
26
|
+
),
|
|
27
|
+
].filter(el => {
|
|
28
|
+
if (el.hasAttribute('disabled')) return false
|
|
29
|
+
if (el.hasAttribute('hidden')) return false
|
|
30
|
+
if (window.getComputedStyle(el).display === 'none') return false
|
|
31
|
+
|
|
32
|
+
return true
|
|
33
|
+
})
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
get keyboard() {
|
|
37
|
+
return this.all.filter(el => el.tabIndex > -1)
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
get first() {
|
|
41
|
+
return this.keyboard[0]
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
get last() {
|
|
45
|
+
return this.keyboard[this.length - 1]
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
next(index) {
|
|
49
|
+
return this.keyboard[index + 1] || null
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
prev(index) {
|
|
53
|
+
return this.keyboard[index + 1] || null
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
}
|
package/dom/font-size.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { splitUnit } from '../lib/index.js'
|
|
2
|
+
|
|
3
|
+
/* globals getComputedStyle */
|
|
4
|
+
export function em(element = document.body, multiple = 1) {
|
|
5
|
+
const unit = parseFloat(getComputedStyle(element)['font-size'])
|
|
6
|
+
return Math.round(unit * multiple)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function rem(multiple = 1) {
|
|
10
|
+
const unit = parseFloat(getComputedStyle(document.body)['font-size'])
|
|
11
|
+
return Math.round(unit * multiple)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function toPx(value, element = null) {
|
|
15
|
+
const [amt, unit] = splitUnit(value)
|
|
16
|
+
let finalValue = 0
|
|
17
|
+
|
|
18
|
+
if (unit === 'rem') finalValue = rem(amt)
|
|
19
|
+
if (unit === 'em') finalValue = em(element, amt)
|
|
20
|
+
if (unit === 'px') finalValue = amt
|
|
21
|
+
if (!unit) finalValue = amt // Default to px offsets
|
|
22
|
+
|
|
23
|
+
return finalValue
|
|
24
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Returns the target HTMLElement.
|
|
2
|
+
// If the selector is already an HTMLElement, it will return the selector itself.
|
|
3
|
+
// Otherwise, it will return the first element that matches the selector.
|
|
4
|
+
|
|
5
|
+
export function getElement(selector) {
|
|
6
|
+
if (!selector) return
|
|
7
|
+
if (selector instanceof HTMLElement) return selector
|
|
8
|
+
return document.querySelector(selector)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function getChildrenElements(node) {
|
|
12
|
+
let children = node.children
|
|
13
|
+
|
|
14
|
+
// Compensate for Astro Slots
|
|
15
|
+
if (children[0]?.nodeName === 'ASTRO-SLOT') children = children[0].children
|
|
16
|
+
return Array.from(children)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getSiblingElements(element) {
|
|
20
|
+
return Array.from(element.parentElement.children).filter(
|
|
21
|
+
child => child !== element,
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function getSelfIndex(element) {
|
|
26
|
+
return Array.from(element.parentElement.children).indexOf(element)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function isAncestor(ancestor, element, { searchLevels = 0 } = {}) {
|
|
30
|
+
if (ancestor === element) return true
|
|
31
|
+
|
|
32
|
+
let parent = element.parentElement
|
|
33
|
+
// Infinite Search
|
|
34
|
+
if (searchLevels === 0) {
|
|
35
|
+
while (parent) {
|
|
36
|
+
if (ancestor === parent) return true
|
|
37
|
+
parent = parent.parentElement
|
|
38
|
+
}
|
|
39
|
+
} else {
|
|
40
|
+
// Search X levels upwards
|
|
41
|
+
for (let i = 0; i < searchLevels; i++) {
|
|
42
|
+
if (ancestor === parent) return true
|
|
43
|
+
parent = parent.parentElement
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return false
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function getAncestorWithSiblings(element, { limit = false } = {}) {
|
|
51
|
+
const ancestor = element.parentElement
|
|
52
|
+
const siblings = getSiblingElements(ancestor)
|
|
53
|
+
|
|
54
|
+
// Found
|
|
55
|
+
if (siblings.length) return siblings
|
|
56
|
+
|
|
57
|
+
// No more ancestors
|
|
58
|
+
if (!ancestor) return null
|
|
59
|
+
|
|
60
|
+
// Unlimited upwards search
|
|
61
|
+
if (limit === false) return getAncestorWithSiblings(ancestor)
|
|
62
|
+
|
|
63
|
+
// Limited search
|
|
64
|
+
if (limit > 0)
|
|
65
|
+
return getAncestorWithSiblings(ancestor, { searchLevels: limit - 1 })
|
|
66
|
+
|
|
67
|
+
// Limit reached, end search.
|
|
68
|
+
if (limit === 0) return null
|
|
69
|
+
}
|
package/dom/hash.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export async function sha256Hash(string) {
|
|
2
|
+
const utf8 = new TextEncoder().encode(string)
|
|
3
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', utf8)
|
|
4
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer))
|
|
5
|
+
const hashHex = hashArray
|
|
6
|
+
.map(bytes => bytes.toString(16).padStart(2, '0'))
|
|
7
|
+
.join('')
|
|
8
|
+
return hashHex
|
|
9
|
+
}
|
package/dom/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export * from './bounding-box.js'
|
|
2
|
+
export * from './clipboard.js'
|
|
3
|
+
export * from './cookie.js'
|
|
4
|
+
export * from './css-vars.js'
|
|
5
|
+
export * from './events.js'
|
|
6
|
+
export * from './focusable.js'
|
|
7
|
+
export * from './font-size.js'
|
|
8
|
+
export * from './get-element.js'
|
|
9
|
+
export * from './hash.js'
|
|
10
|
+
export * from './keyboard.js'
|
|
11
|
+
export * from './local-store.js'
|
|
12
|
+
export * from './media.js'
|
|
13
|
+
export * from './pkce.js'
|
|
14
|
+
export * from './query-params.js'
|
|
15
|
+
export * from './random-string.js'
|
|
16
|
+
export * from './session-store.js'
|
|
17
|
+
export * from './trap-focus.js'
|
|
18
|
+
export * from './ui/index.js'
|
package/dom/keyboard.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event is Tab (without shift)
|
|
3
|
+
* @param {Object} event - The event object
|
|
4
|
+
* @returns {Boolean}
|
|
5
|
+
*/
|
|
6
|
+
export function isTab(event) {
|
|
7
|
+
return !event.shiftKey && event.key === 'Tab'
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Event is shift + Tab
|
|
12
|
+
* @param {Object} event - The event object
|
|
13
|
+
* @returns {Boolean}
|
|
14
|
+
*/
|
|
15
|
+
export function isShiftTab(event) {
|
|
16
|
+
return event.shiftKey && event.key === 'Tab'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Matches both Tab and Shift+Tab
|
|
21
|
+
* @param {Object} event - The event object
|
|
22
|
+
* @returns
|
|
23
|
+
*/
|
|
24
|
+
export function isTabKey(key) {
|
|
25
|
+
return key === 'Tab'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Event is an arrow key
|
|
30
|
+
* @param {Object} event - The event object
|
|
31
|
+
* @returns {Boolean}
|
|
32
|
+
*/
|
|
33
|
+
export function isArrowKey(key) {
|
|
34
|
+
return ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(key)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function isEscapeKey(key) {
|
|
38
|
+
return key === 'Escape'
|
|
39
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// ========================
|
|
2
|
+
// Local Storage
|
|
3
|
+
// ========================
|
|
4
|
+
export const localStore = {
|
|
5
|
+
get(key) {
|
|
6
|
+
const value = localStorage.getItem(key)
|
|
7
|
+
if (!value) return
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
// Returns an object if stored value is JSON
|
|
11
|
+
return JSON.parse(value)
|
|
12
|
+
} catch (error) {
|
|
13
|
+
// Returns plain string if stored value is not JSON
|
|
14
|
+
return value
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
set(key, value) {
|
|
19
|
+
if (typeof value === 'string') {
|
|
20
|
+
localStorage.setItem(key, value)
|
|
21
|
+
} else {
|
|
22
|
+
localStorage.setItem(key, JSON.stringify(value))
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
remove(key) {
|
|
27
|
+
localStorage.removeItem(key)
|
|
28
|
+
},
|
|
29
|
+
}
|
package/dom/media.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export async function imagesLoaded(node) {
|
|
2
|
+
const images = Array.from(node.querySelectorAll('img'))
|
|
3
|
+
const promises = images.map(img => {
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
if (img.complete) return resolve()
|
|
6
|
+
img.onload = resolve
|
|
7
|
+
img.onerror = reject
|
|
8
|
+
})
|
|
9
|
+
})
|
|
10
|
+
return Promise.all(promises)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function videosLoaded(node) {
|
|
14
|
+
const videos = Array.from(node.querySelectorAll('video'))
|
|
15
|
+
const promises = videos.map(video => {
|
|
16
|
+
return new Promise((resolve, reject) => {
|
|
17
|
+
if (video.readyState === 4) return resolve() // HAVE_ENOUGH_DATA
|
|
18
|
+
video.onloadedmetadata = resolve
|
|
19
|
+
video.onerror = reject
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
return Promise.all(promises)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function mediaLoaded(node) {
|
|
26
|
+
return Promise.all([imagesLoaded(node), videosLoaded(node)])
|
|
27
|
+
}
|
package/dom/pkce.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Browser only
|
|
2
|
+
// Check the node version for usage in servers
|
|
3
|
+
import { randomString } from './random-string.js'
|
|
4
|
+
|
|
5
|
+
export async function PKCE() {
|
|
6
|
+
const codeVerifier = await randomString()
|
|
7
|
+
const codeChallenge = await getCodeChallenge(codeVerifier)
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
state: await randomString(),
|
|
11
|
+
code_verifier: codeVerifier,
|
|
12
|
+
code_challenge: codeChallenge,
|
|
13
|
+
code_challenge_method: 'S256',
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Generates code challenge in browser
|
|
19
|
+
* @param {string} verifier
|
|
20
|
+
* @returns promise
|
|
21
|
+
*/
|
|
22
|
+
async function getCodeChallenge(verifier) {
|
|
23
|
+
// Hash the code verifier with SHA-256
|
|
24
|
+
const encoder = new TextEncoder()
|
|
25
|
+
const data = encoder.encode(verifier)
|
|
26
|
+
const hashed = await window.crypto.subtle.digest('SHA-256', data)
|
|
27
|
+
|
|
28
|
+
// Base64url encode the hash
|
|
29
|
+
return window
|
|
30
|
+
.btoa(String.fromCharCode.apply(null, new Uint8Array(hashed)))
|
|
31
|
+
.replace(/\+/g, '-')
|
|
32
|
+
.replace(/\//g, '_')
|
|
33
|
+
.replace(/=+$/, '')
|
|
34
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// ========================
|
|
2
|
+
// Query Params
|
|
3
|
+
// ========================
|
|
4
|
+
export const queryParams = {
|
|
5
|
+
get(key) {
|
|
6
|
+
const searchParams = new URLSearchParams(location.search)
|
|
7
|
+
return searchParams.get(key)
|
|
8
|
+
},
|
|
9
|
+
|
|
10
|
+
stringify(params) {
|
|
11
|
+
const searchParams = new URLSearchParams(location.search)
|
|
12
|
+
return searchParams.toString()
|
|
13
|
+
},
|
|
14
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates a random string.
|
|
3
|
+
* Ensures first character is a letter so it can be used in HTML ids and classes
|
|
4
|
+
* This can actually be used everywhere, not just in the browser.
|
|
5
|
+
*/
|
|
6
|
+
export function randomString(length) {
|
|
7
|
+
const firstLetter = String.fromCharCode(65 + Math.floor(Math.random() * 26))
|
|
8
|
+
const others = Math.random()
|
|
9
|
+
.toString(36)
|
|
10
|
+
.substring(2, 2 + length - 1)
|
|
11
|
+
|
|
12
|
+
return firstLetter + others
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// For browsers only
|
|
16
|
+
export function uuid() {
|
|
17
|
+
return crypto.randomUUID()
|
|
18
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// ========================
|
|
2
|
+
// Session Storage
|
|
3
|
+
// ========================
|
|
4
|
+
export const sessionStore = {
|
|
5
|
+
get(key) {
|
|
6
|
+
const value = sessionStorage.getItem(key)
|
|
7
|
+
if (!value) return
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
// Returns an object if stored value is JSON
|
|
11
|
+
return JSON.parse(value)
|
|
12
|
+
} catch (error) {
|
|
13
|
+
// Returns plain string if stored value is not JSON
|
|
14
|
+
return value
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
set(key, value) {
|
|
19
|
+
if (typeof value === 'string') {
|
|
20
|
+
sessionStorage.setItem(key, value)
|
|
21
|
+
} else {
|
|
22
|
+
sessionStorage.setItem(key, JSON.stringify(value))
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
remove(key) {
|
|
27
|
+
sessionStorage.removeItem(key)
|
|
28
|
+
},
|
|
29
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Focusable } from './focusable.js'
|
|
2
|
+
import { isShiftTab, isTab } from './keyboard.js'
|
|
3
|
+
|
|
4
|
+
export function trapFocus({ event, focusables }) {
|
|
5
|
+
if (event.key !== 'Tab') return
|
|
6
|
+
if (isTab(event) && document.activeElement === focusables.last) {
|
|
7
|
+
event.preventDefault()
|
|
8
|
+
focusables.first.focus()
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// User presses shift + tab from the first focusable element.
|
|
12
|
+
// Redirects to the last focusable element.
|
|
13
|
+
if (isShiftTab(event) && document.activeElement === focusables.first) {
|
|
14
|
+
event.preventDefault()
|
|
15
|
+
focusables.last.focus()
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/* Returns focus back to trigger node when user is done tabbing through the content */
|
|
20
|
+
export function manageFocus({
|
|
21
|
+
event,
|
|
22
|
+
triggerNode,
|
|
23
|
+
contentNode,
|
|
24
|
+
focusables,
|
|
25
|
+
tabOutFocus = 'next', // 'trigger'
|
|
26
|
+
onLeave = function () {},
|
|
27
|
+
}) {
|
|
28
|
+
const activeEl = document.activeElement
|
|
29
|
+
|
|
30
|
+
if (activeEl === contentNode && isShiftTab(event)) return backFocus()
|
|
31
|
+
if (activeEl === contentNode && focusables.length === 0 && isTab(event))
|
|
32
|
+
return nextFocus()
|
|
33
|
+
|
|
34
|
+
if (activeEl === focusables.last && isTab(event)) return nextFocus()
|
|
35
|
+
if (activeEl === focusables.first && isShiftTab(event)) return backFocus()
|
|
36
|
+
|
|
37
|
+
function backFocus() {
|
|
38
|
+
event.preventDefault()
|
|
39
|
+
triggerNode.focus()
|
|
40
|
+
onLeave('back')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function nextFocus() {
|
|
44
|
+
if (tabOutFocus === 'trigger') triggerNode.focus()
|
|
45
|
+
if (tabOutFocus === 'next') {
|
|
46
|
+
event.preventDefault()
|
|
47
|
+
const DOMFocusables = Focusable().keyboard
|
|
48
|
+
const index = DOMFocusables.findIndex(e => e === triggerNode)
|
|
49
|
+
const next = DOMFocusables[index + 1]
|
|
50
|
+
if (next) next.focus()
|
|
51
|
+
else triggerNode.focus()
|
|
52
|
+
}
|
|
53
|
+
onLeave('forward')
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function markActiveAnchorIn(container, anchorType = 'page') {
|
|
2
|
+
if (!container) return
|
|
3
|
+
|
|
4
|
+
const { pathname } = window.location
|
|
5
|
+
const element = container.querySelector(`[href="${pathname}"]`)
|
|
6
|
+
if (!element) return
|
|
7
|
+
|
|
8
|
+
// Mark the active element
|
|
9
|
+
element.setAttribute('aria-current', 'page')
|
|
10
|
+
return element
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function goToElement(
|
|
14
|
+
element,
|
|
15
|
+
{ scrollBehaviour = 'instant', scrollMargin = '3rem' } = {},
|
|
16
|
+
) {
|
|
17
|
+
if (!element) return
|
|
18
|
+
element.style.scrollMarginTop = scrollMargin
|
|
19
|
+
element.scrollIntoView({ behavior: scrollBehaviour })
|
|
20
|
+
}
|
package/dom/ui/index.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function findScrollContainer(element, direction = 'both') {
|
|
2
|
+
if (!element) return
|
|
3
|
+
|
|
4
|
+
let parent = element.parentElement
|
|
5
|
+
while (parent) {
|
|
6
|
+
const { overflow, overflowX, overflowY } = getComputedStyle(parent)
|
|
7
|
+
|
|
8
|
+
if (direction === 'both') {
|
|
9
|
+
if (overflow.includes('auto') || overflow.includes('scroll'))
|
|
10
|
+
return parent
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (direction === 'horizontal') {
|
|
14
|
+
if (overflowX === 'auto' || overflowX === 'scroll') return parent
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (direction === 'vertical') {
|
|
18
|
+
if (overflowY === 'auto' || overflowY === 'scroll') return parent
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
parent = parent.parentElement
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return document.documentElement
|
|
25
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Adjusted function to take excludes as an argument
|
|
2
|
+
export function traverseAndScramble(node, { excludes } = {}) {
|
|
3
|
+
// 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
|
+
) {
|
|
8
|
+
return // Skip this node and its children
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
node.childNodes.forEach(child => {
|
|
12
|
+
if (child.nodeType === 3) {
|
|
13
|
+
// Node.TEXT_NODE
|
|
14
|
+
child.nodeValue = scrambleText(child.nodeValue)
|
|
15
|
+
} else if (child.nodeType === 1) {
|
|
16
|
+
// Node.ELEMENT_NODE
|
|
17
|
+
traverseAndScramble(child, { excludes }) // Recurse into child elements with excludes
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Function to scramble text remains unchanged
|
|
23
|
+
function scrambleText(text) {
|
|
24
|
+
return text
|
|
25
|
+
.split('')
|
|
26
|
+
.map(char => {
|
|
27
|
+
if (/^[A-Za-z0-9]$/.test(char)) {
|
|
28
|
+
const randomCharCode =
|
|
29
|
+
Math.random() < 26 / 36
|
|
30
|
+
? Math.floor(Math.random() * 26) +
|
|
31
|
+
(char === char.toUpperCase() ? 65 : 97) // A-Z
|
|
32
|
+
: Math.floor(Math.random() * 10) + 48 // 0-9
|
|
33
|
+
return String.fromCharCode(randomCharCode)
|
|
34
|
+
} else {
|
|
35
|
+
// Keep punctuations and other symbols intact
|
|
36
|
+
return char
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
.join('')
|
|
40
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './route-manager.js'
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
// Usage ... Probably should use either protected or bypass, not both.
|
|
3
|
+
@example
|
|
4
|
+
const config = {
|
|
5
|
+
routeConfig: {
|
|
6
|
+
startsWith: ['/dashboard']
|
|
7
|
+
}
|
|
8
|
+
bypassRoutes: {
|
|
9
|
+
is: ['/'],
|
|
10
|
+
startsWith: ['/fonts', '/auth', '/favicon', '/images'],
|
|
11
|
+
includes: ['/meh'],
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const {isProtectedRoute, isBypassRoute} = AuthManager(config)
|
|
15
|
+
if (!isProtectedRoute(context.url.pathname)) return next() // Handle only protected routes
|
|
16
|
+
if (isBypassRoute(context.url.pathname)) return next() // Bypass all non protected routes
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export function RouteManager(config) {
|
|
20
|
+
const { protectedRoutes, bypassRoutes } = config
|
|
21
|
+
return {
|
|
22
|
+
isProtectedRoute(pathname) {
|
|
23
|
+
return isMatch(protectedRoutes, pathname)
|
|
24
|
+
},
|
|
25
|
+
isBypassRoute(pathname) {
|
|
26
|
+
return isMatch(bypassRoutes, pathname)
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Alias for RouteManager
|
|
32
|
+
export function AuthManager(config) {
|
|
33
|
+
return RouteManager(config)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isMatch(routeConfig, pathname) {
|
|
37
|
+
const { is, startsWith, includes } = routeConfig
|
|
38
|
+
const isExactMatch = is && is.some(u => pathname === u)
|
|
39
|
+
const startsWithMatch =
|
|
40
|
+
startsWith && startsWith.some(u => pathname.startsWith(u))
|
|
41
|
+
const includesMatch = includes && includes.some(u => pathname.includes(u))
|
|
42
|
+
|
|
43
|
+
return isExactMatch || startsWithMatch || includesMatch
|
|
44
|
+
}
|
package/lib/checks.js
ADDED
package/lib/date/days.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const DAYS = [
|
|
2
|
+
{ short: 'Sun', long: 'Sunday', numeric: 1 },
|
|
3
|
+
{ short: 'Mon', long: 'Monday', numeric: 2 },
|
|
4
|
+
{ short: 'Tue', long: 'Tuesday', numeric: 3 },
|
|
5
|
+
{ short: 'Wed', long: 'Wednesday', numeric: 4 },
|
|
6
|
+
{ short: 'Thu', long: 'Thursday', numeric: 5 },
|
|
7
|
+
{ short: 'Fri', long: 'Friday', numeric: 6 },
|
|
8
|
+
{ short: 'Sat', long: 'Saturday', numeric: 7 },
|
|
9
|
+
]
|