@rokkit/actions 1.0.0-next.94 → 1.0.0-next.96
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/package.json +16 -16
- package/src/fillable.js +11 -11
- package/src/hierarchy.js +3 -3
- package/src/index.js +1 -3
- package/src/lib/index.js +1 -1
- package/src/lib/internal.js +4 -5
- package/src/lib/viewport.js +20 -19
- package/src/navigable.js +13 -12
- package/src/navigator.js +17 -15
- package/src/pannable.js +32 -23
- package/src/swipeable.js +64 -20
- package/src/switchable.js +27 -15
- package/src/traversable.js +370 -77
- package/src/types.js +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rokkit/actions",
|
|
3
|
-
"version": "1.0.0-next.
|
|
3
|
+
"version": "1.0.0-next.96",
|
|
4
4
|
"description": "Contains generic actions that can be used in various components.",
|
|
5
5
|
"author": "Jerry Thomas <me@jerrythomas.name>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -12,18 +12,18 @@
|
|
|
12
12
|
"access": "public"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
|
-
"@sveltejs/vite-plugin-svelte": "^3.
|
|
16
|
-
"@testing-library/svelte": "^
|
|
17
|
-
"@types/ramda": "^0.
|
|
18
|
-
"@vitest/coverage-v8": "^1.
|
|
19
|
-
"@vitest/ui": "~1.
|
|
20
|
-
"jsdom": "^24.
|
|
21
|
-
"svelte": "^4.2.
|
|
22
|
-
"typescript": "^5.4.
|
|
23
|
-
"vite": "^5.2.
|
|
24
|
-
"vitest": "~1.
|
|
25
|
-
"shared-config": "1.0.0-next.
|
|
26
|
-
"validators": "1.0.0-next.
|
|
15
|
+
"@sveltejs/vite-plugin-svelte": "^3.1.1",
|
|
16
|
+
"@testing-library/svelte": "^5.1.0",
|
|
17
|
+
"@types/ramda": "^0.30.0",
|
|
18
|
+
"@vitest/coverage-v8": "^1.6.0",
|
|
19
|
+
"@vitest/ui": "~1.6.0",
|
|
20
|
+
"jsdom": "^24.1.0",
|
|
21
|
+
"svelte": "^4.2.17",
|
|
22
|
+
"typescript": "^5.4.5",
|
|
23
|
+
"vite": "^5.2.12",
|
|
24
|
+
"vitest": "~1.6.0",
|
|
25
|
+
"shared-config": "1.0.0-next.96",
|
|
26
|
+
"validators": "1.0.0-next.96"
|
|
27
27
|
},
|
|
28
28
|
"files": [
|
|
29
29
|
"src/**/*.js",
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
}
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"ramda": "^0.
|
|
43
|
-
"@rokkit/core": "1.0.0-next.
|
|
44
|
-
"@rokkit/stores": "1.0.0-next.
|
|
42
|
+
"ramda": "^0.30.1",
|
|
43
|
+
"@rokkit/core": "1.0.0-next.96",
|
|
44
|
+
"@rokkit/stores": "1.0.0-next.96"
|
|
45
45
|
},
|
|
46
46
|
"scripts": {
|
|
47
47
|
"format": "prettier --write .",
|
package/src/fillable.js
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* @returns
|
|
7
7
|
*/
|
|
8
8
|
export function fillable(node, { options, current, check }) {
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
const data = { options, current, check }
|
|
10
|
+
const blanks = node.getElementsByTagName('del')
|
|
11
11
|
|
|
12
12
|
function click(event) {
|
|
13
13
|
if (event.target.innerHTML !== '?') {
|
|
@@ -18,16 +18,16 @@ export function fillable(node, { options, current, check }) {
|
|
|
18
18
|
initialize(blanks, click)
|
|
19
19
|
|
|
20
20
|
return {
|
|
21
|
-
update(
|
|
22
|
-
data.options = options
|
|
23
|
-
data.current = current
|
|
21
|
+
update(input) {
|
|
22
|
+
data.options = input.options
|
|
23
|
+
data.current = input.current
|
|
24
24
|
data.check = check
|
|
25
25
|
|
|
26
26
|
fill(blanks, data.options, data.current)
|
|
27
27
|
if (data.check) validate(blanks, data)
|
|
28
28
|
},
|
|
29
29
|
destroy() {
|
|
30
|
-
Object.keys(blanks).
|
|
30
|
+
Object.keys(blanks).forEach((ref) => {
|
|
31
31
|
blanks[ref].removeEventListener('click', click)
|
|
32
32
|
})
|
|
33
33
|
}
|
|
@@ -41,10 +41,10 @@ export function fillable(node, { options, current, check }) {
|
|
|
41
41
|
* @param {EventListener} click
|
|
42
42
|
*/
|
|
43
43
|
function initialize(blanks, click) {
|
|
44
|
-
Object.keys(blanks).
|
|
44
|
+
Object.keys(blanks).forEach((ref) => {
|
|
45
45
|
blanks[ref].addEventListener('click', click)
|
|
46
46
|
blanks[ref].classList.add('empty')
|
|
47
|
-
blanks[ref].name =
|
|
47
|
+
blanks[ref].name = `fill-${ref}`
|
|
48
48
|
blanks[ref]['data-index'] = ref
|
|
49
49
|
})
|
|
50
50
|
}
|
|
@@ -58,7 +58,7 @@ function initialize(blanks, click) {
|
|
|
58
58
|
*/
|
|
59
59
|
function fill(blanks, options, current) {
|
|
60
60
|
if (current > -1 && current < Object.keys(blanks).length) {
|
|
61
|
-
|
|
61
|
+
const index = options.findIndex(({ actualIndex }) => actualIndex === current)
|
|
62
62
|
if (index > -1) {
|
|
63
63
|
blanks[current].innerHTML = options[index].value
|
|
64
64
|
blanks[current].classList.remove('empty')
|
|
@@ -96,8 +96,8 @@ function clear(event, node) {
|
|
|
96
96
|
* @param {import('./types').FillOptions} data
|
|
97
97
|
*/
|
|
98
98
|
function validate(blanks, data) {
|
|
99
|
-
Object.keys(blanks).
|
|
100
|
-
|
|
99
|
+
Object.keys(blanks).forEach((_, ref) => {
|
|
100
|
+
const index = data.options.findIndex(({ actualIndex }) => actualIndex === ref)
|
|
101
101
|
if (index > -1)
|
|
102
102
|
blanks[ref].classList.add(
|
|
103
103
|
data.options[index].expectedIndex === data.options[index].actualIndex ? 'pass' : 'fail'
|
package/src/hierarchy.js
CHANGED
|
@@ -100,9 +100,9 @@ export function movePrevious(path) {
|
|
|
100
100
|
* @returns
|
|
101
101
|
*/
|
|
102
102
|
export function pathFromIndices(indices, items, fields) {
|
|
103
|
-
|
|
104
|
-
let fragment
|
|
105
|
-
indices.
|
|
103
|
+
const path = []
|
|
104
|
+
let fragment = {}
|
|
105
|
+
indices.forEach((index, level) => {
|
|
106
106
|
if (level === 0) {
|
|
107
107
|
fragment = { index, items, fields }
|
|
108
108
|
} else {
|
package/src/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
export * from './lib/constants'
|
|
1
|
+
export * from './types'
|
|
3
2
|
export * from './lib'
|
|
4
3
|
export { fillable } from './fillable'
|
|
5
4
|
export { pannable } from './pannable'
|
|
@@ -10,4 +9,3 @@ export { themable } from './themeable'
|
|
|
10
9
|
export { swipeable } from './swipeable'
|
|
11
10
|
export { switchable } from './switchable'
|
|
12
11
|
export { delegateKeyboardEvents } from './delegate'
|
|
13
|
-
export { virtualListViewport } from './lib/viewport'
|
package/src/lib/index.js
CHANGED
package/src/lib/internal.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { compact } from '@rokkit/core'
|
|
2
|
-
import { hasChildren, isExpanded } from '@rokkit/core'
|
|
1
|
+
import { compact, hasChildren, isExpanded } from '@rokkit/core'
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
4
|
* Emits a custom event with the given data.
|
|
@@ -27,8 +26,8 @@ export function mapKeyboardEventsToActions(handlers, options) {
|
|
|
27
26
|
nested: false,
|
|
28
27
|
...options
|
|
29
28
|
}
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
const expand = nested ? handlers.expand : null
|
|
30
|
+
const collapse = nested ? handlers.collapse : null
|
|
32
31
|
|
|
33
32
|
return compact({
|
|
34
33
|
ArrowDown: horizontal ? expand : next,
|
|
@@ -140,7 +139,7 @@ export function calculateSum(sizes, lower, upper, defaultSize = 40, gap = 0) {
|
|
|
140
139
|
* @returns {Array<number|null>}
|
|
141
140
|
*/
|
|
142
141
|
export function updateSizes(sizes, values, offset = 0) {
|
|
143
|
-
|
|
142
|
+
const result = [...sizes.slice(0, offset), ...values, ...sizes.slice(offset + values.length)]
|
|
144
143
|
|
|
145
144
|
return result
|
|
146
145
|
}
|
package/src/lib/viewport.js
CHANGED
|
@@ -15,13 +15,32 @@ export function virtualListViewport(options) {
|
|
|
15
15
|
before: 0,
|
|
16
16
|
after: 0
|
|
17
17
|
})
|
|
18
|
-
let items
|
|
18
|
+
let items = null
|
|
19
19
|
let averageSize = minSize
|
|
20
20
|
let visibleCount = maxVisible
|
|
21
21
|
let value = null
|
|
22
22
|
let cache = []
|
|
23
23
|
let index = -1
|
|
24
24
|
|
|
25
|
+
const updateBounds = ({ lower, upper }) => {
|
|
26
|
+
const previous = get(bounds)
|
|
27
|
+
if (maxVisible > 0) {
|
|
28
|
+
const visible = calculateSum(cache, lower, upper, averageSize, gap)
|
|
29
|
+
space.update((state) => (state = { ...state, visible }))
|
|
30
|
+
}
|
|
31
|
+
if (previous.lower !== lower) {
|
|
32
|
+
const before = calculateSum(cache, 0, lower, averageSize)
|
|
33
|
+
space.update((state) => (state = { ...state, before }))
|
|
34
|
+
}
|
|
35
|
+
if (previous.upper !== upper) {
|
|
36
|
+
const after = calculateSum(cache, upper, cache.length, averageSize)
|
|
37
|
+
space.update((state) => (state = { ...state, after }))
|
|
38
|
+
}
|
|
39
|
+
if (previous.lower !== lower || previous.upper !== upper) {
|
|
40
|
+
bounds.set({ lower, upper })
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
25
44
|
const update = (data) => {
|
|
26
45
|
// const previous = get(bounds)
|
|
27
46
|
|
|
@@ -76,24 +95,6 @@ export function virtualListViewport(options) {
|
|
|
76
95
|
updateBounds(current)
|
|
77
96
|
}
|
|
78
97
|
}
|
|
79
|
-
const updateBounds = ({ lower, upper }) => {
|
|
80
|
-
const previous = get(bounds)
|
|
81
|
-
if (maxVisible > 0) {
|
|
82
|
-
let visible = calculateSum(cache, lower, upper, averageSize, gap)
|
|
83
|
-
space.update((value) => (value = { ...value, visible }))
|
|
84
|
-
}
|
|
85
|
-
if (previous.lower !== lower) {
|
|
86
|
-
let before = calculateSum(cache, 0, lower, averageSize)
|
|
87
|
-
space.update((value) => (value = { ...value, before }))
|
|
88
|
-
}
|
|
89
|
-
if (previous.upper !== upper) {
|
|
90
|
-
let after = calculateSum(cache, upper, cache.length, averageSize)
|
|
91
|
-
space.update((value) => (value = { ...value, after }))
|
|
92
|
-
}
|
|
93
|
-
if (previous.lower !== lower || previous.upper !== upper) {
|
|
94
|
-
bounds.set({ lower, upper })
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
98
|
|
|
98
99
|
const scrollTo = (position) => {
|
|
99
100
|
const start = Math.round(position / averageSize)
|
package/src/navigable.js
CHANGED
|
@@ -23,23 +23,24 @@ export function navigable(node, options) {
|
|
|
23
23
|
let actions = {} //getKeyboardActions(node, { horizontal, nested })
|
|
24
24
|
const handleKeydown = (event) => handleAction(actions, event)
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Update the listeners based on the input configuration.
|
|
28
|
+
* @param {import('./types').NavigableOptions} input
|
|
29
|
+
*/
|
|
30
|
+
function updateListeners(input) {
|
|
31
|
+
options = { ...options, ...input }
|
|
32
|
+
if (listening) node.removeEventListener('keydown', handleKeydown)
|
|
28
33
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
34
|
+
actions = getKeyboardActions(node, input, handlers)
|
|
35
|
+
if (input.enabled) node.addEventListener('keydown', handleKeydown)
|
|
36
|
+
|
|
37
|
+
listening = input.enabled
|
|
32
38
|
}
|
|
33
39
|
|
|
34
40
|
updateListeners(options)
|
|
35
41
|
|
|
36
42
|
return {
|
|
37
|
-
update: (config) =>
|
|
38
|
-
|
|
39
|
-
updateListeners(options)
|
|
40
|
-
},
|
|
41
|
-
destroy: () => {
|
|
42
|
-
updateListeners({ enabled: false })
|
|
43
|
-
}
|
|
43
|
+
update: (config) => updateListeners(config),
|
|
44
|
+
destroy: () => updateListeners({ enabled: false })
|
|
44
45
|
}
|
|
45
46
|
}
|
package/src/navigator.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { handleAction } from './utils'
|
|
2
|
-
import { isNested, hasChildren, isExpanded } from '@rokkit/core'
|
|
2
|
+
import { noop, isNested, hasChildren, isExpanded } from '@rokkit/core'
|
|
3
3
|
import {
|
|
4
4
|
moveNext,
|
|
5
5
|
movePrevious,
|
|
@@ -18,21 +18,23 @@ import { mapKeyboardEventsToActions } from './lib'
|
|
|
18
18
|
*/
|
|
19
19
|
export function navigator(element, options) {
|
|
20
20
|
const { fields, enabled = true, vertical = true, idPrefix = 'id-' } = options
|
|
21
|
-
let items
|
|
21
|
+
let items = [],
|
|
22
|
+
path = null,
|
|
23
|
+
currentNode = null
|
|
22
24
|
|
|
23
|
-
if (!enabled) return { destroy:
|
|
25
|
+
if (!enabled) return { destroy: noop }
|
|
24
26
|
|
|
25
27
|
// todo: Update should handle selection value change
|
|
26
28
|
// should we wait a tick before updating?
|
|
27
|
-
const update = (
|
|
29
|
+
const update = (input) => {
|
|
28
30
|
const previousNode = currentNode
|
|
29
|
-
items =
|
|
30
|
-
path = pathFromIndices(
|
|
31
|
+
items = input.items
|
|
32
|
+
path = pathFromIndices(input.indices ?? [], items, fields)
|
|
31
33
|
currentNode = getCurrentNode(path)
|
|
32
34
|
|
|
33
35
|
if (previousNode !== currentNode && currentNode) {
|
|
34
36
|
const indices = indicesFromPath(path)
|
|
35
|
-
|
|
37
|
+
const current = element.querySelector(`#${idPrefix}${indices.join('-')}`)
|
|
36
38
|
if (current) {
|
|
37
39
|
current.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
|
|
38
40
|
}
|
|
@@ -59,8 +61,8 @@ export function navigator(element, options) {
|
|
|
59
61
|
}
|
|
60
62
|
const collapse = () => {
|
|
61
63
|
if (currentNode) {
|
|
62
|
-
const
|
|
63
|
-
if (
|
|
64
|
+
const expanded = isExpanded(currentNode, path[path.length - 1].fields)
|
|
65
|
+
if (expanded) {
|
|
64
66
|
toggle()
|
|
65
67
|
} else if (path.length > 0) {
|
|
66
68
|
path = path.slice(0, -1)
|
|
@@ -94,8 +96,8 @@ export function navigator(element, options) {
|
|
|
94
96
|
|
|
95
97
|
const handleClick = (event) => {
|
|
96
98
|
event.stopPropagation()
|
|
97
|
-
|
|
98
|
-
|
|
99
|
+
const target = findParentWithDataPath(event.target, element)
|
|
100
|
+
const indices = !target
|
|
99
101
|
? []
|
|
100
102
|
: target.dataset.path
|
|
101
103
|
.split(',')
|
|
@@ -108,8 +110,8 @@ export function navigator(element, options) {
|
|
|
108
110
|
if (hasChildren(currentNode, path[path.length - 1].fields)) {
|
|
109
111
|
currentNode[path[path.length - 1].fields.isOpen] =
|
|
110
112
|
!currentNode[path[path.length - 1].fields.isOpen]
|
|
111
|
-
const
|
|
112
|
-
emit(
|
|
113
|
+
const eventName = currentNode[path[path.length - 1].fields.isOpen] ? 'expand' : 'collapse'
|
|
114
|
+
emit(eventName, element, indices, currentNode)
|
|
113
115
|
} else if (currentNode !== null) emit('select', element, indices, currentNode)
|
|
114
116
|
emit('move', element, indices, currentNode)
|
|
115
117
|
// emit('select', element, indices, currentNode)
|
|
@@ -138,7 +140,7 @@ export function navigator(element, options) {
|
|
|
138
140
|
*/
|
|
139
141
|
export function moveTo(element, path, currentNode, idPrefix) {
|
|
140
142
|
const indices = indicesFromPath(path)
|
|
141
|
-
|
|
143
|
+
const current = element.querySelector(`#${idPrefix}${indices.join('-')}`)
|
|
142
144
|
if (current) current.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
|
|
143
145
|
|
|
144
146
|
emit('move', element, indices, currentNode)
|
|
@@ -175,7 +177,7 @@ function emit(event, element, indices, node) {
|
|
|
175
177
|
new CustomEvent(event, {
|
|
176
178
|
detail: {
|
|
177
179
|
path: indices,
|
|
178
|
-
node
|
|
180
|
+
node
|
|
179
181
|
}
|
|
180
182
|
})
|
|
181
183
|
)
|
package/src/pannable.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
+
import { omit } from 'ramda'
|
|
1
2
|
import { removeListeners, setupListeners } from './lib'
|
|
2
3
|
/**
|
|
3
|
-
*
|
|
4
|
+
* Makes an element pannable with mouse or touch events.
|
|
4
5
|
*
|
|
5
|
-
* @param {HTMLElement} node
|
|
6
|
+
* @param {HTMLElement} node The DOM element to apply the panning action.
|
|
6
7
|
* @returns {import('./types').SvelteActionReturn}
|
|
7
8
|
*/
|
|
8
9
|
export function pannable(node) {
|
|
9
|
-
let x
|
|
10
|
-
|
|
11
|
-
let listeners = {
|
|
10
|
+
let coords = { x: 0, y: 0 }
|
|
11
|
+
const listeners = {
|
|
12
12
|
primary: {
|
|
13
13
|
mousedown: start,
|
|
14
14
|
touchstart: start
|
|
@@ -21,32 +21,17 @@ export function pannable(node) {
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
function track(event, name, delta = {}) {
|
|
25
|
-
x = event.clientX || event.touches[0].clientX
|
|
26
|
-
y = event.clientY || event.touches[0].clientY
|
|
27
|
-
event.stopPropagation()
|
|
28
|
-
event.preventDefault()
|
|
29
|
-
node.dispatchEvent(
|
|
30
|
-
new CustomEvent(name, {
|
|
31
|
-
detail: { x, y, ...delta }
|
|
32
|
-
})
|
|
33
|
-
)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
24
|
function start(event) {
|
|
37
|
-
|
|
25
|
+
coords = handleEvent(node, event, 'panstart', coords)
|
|
38
26
|
setupListeners(window, listeners.secondary)
|
|
39
27
|
}
|
|
40
28
|
|
|
41
29
|
function move(event) {
|
|
42
|
-
|
|
43
|
-
const dy = (event.clientY || event.touches[0].clientY) - y
|
|
44
|
-
|
|
45
|
-
track(event, 'panmove', { dx, dy })
|
|
30
|
+
coords = handleEvent(node, event, 'panmove', coords)
|
|
46
31
|
}
|
|
47
32
|
|
|
48
33
|
function stop(event) {
|
|
49
|
-
|
|
34
|
+
coords = handleEvent(node, event, 'panend', coords)
|
|
50
35
|
removeListeners(window, listeners.secondary)
|
|
51
36
|
}
|
|
52
37
|
|
|
@@ -56,3 +41,27 @@ export function pannable(node) {
|
|
|
56
41
|
destroy: () => removeListeners(node, listeners.primary)
|
|
57
42
|
}
|
|
58
43
|
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Handles the panning event.
|
|
47
|
+
*
|
|
48
|
+
* @param {HTMLElement} node - The node where the event is dispatched.
|
|
49
|
+
* @param {Event} event - The event object.
|
|
50
|
+
* @param {string} name - The name of the event.
|
|
51
|
+
* @param {import('./types').Coords} coords - The previous coordinates of the event.
|
|
52
|
+
*/
|
|
53
|
+
function handleEvent(node, event, name, coords) {
|
|
54
|
+
const x = event.clientX || event.touches[0].clientX
|
|
55
|
+
const y = event.clientY || event.touches[0].clientY
|
|
56
|
+
const detail = { x, y }
|
|
57
|
+
|
|
58
|
+
if (name === 'panmove') {
|
|
59
|
+
detail.dx = x - coords.x
|
|
60
|
+
detail.dy = y - coords.y
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
event.stopPropagation()
|
|
64
|
+
event.preventDefault()
|
|
65
|
+
node.dispatchEvent(new CustomEvent(name, { detail }))
|
|
66
|
+
return omit(['dx', 'dy'], detail)
|
|
67
|
+
}
|
package/src/swipeable.js
CHANGED
|
@@ -16,13 +16,13 @@ const defaultOptions = {
|
|
|
16
16
|
* @returns {import('./types').SvelteActionReturn}
|
|
17
17
|
*/
|
|
18
18
|
export function swipeable(node, options = defaultOptions) {
|
|
19
|
-
|
|
19
|
+
const track = {}
|
|
20
20
|
let listeners = {}
|
|
21
21
|
|
|
22
|
-
const updateListeners = (
|
|
22
|
+
const updateListeners = (props) => {
|
|
23
23
|
removeListeners(node, listeners)
|
|
24
|
-
listeners = getListeners(node,
|
|
25
|
-
setupListeners(node, listeners,
|
|
24
|
+
listeners = getListeners(node, props, track)
|
|
25
|
+
setupListeners(node, listeners, props)
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
options = { ...defaultOptions, ...options }
|
|
@@ -49,7 +49,7 @@ export function swipeable(node, options = defaultOptions) {
|
|
|
49
49
|
function getListeners(node, options, track) {
|
|
50
50
|
if (!options.enabled) return {}
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
const listeners = {
|
|
53
53
|
touchend: (e) => touchEnd(e, node, options, track),
|
|
54
54
|
touchstart: (e) => touchStart(e, track),
|
|
55
55
|
mousedown: (e) => touchStart(e, track),
|
|
@@ -72,31 +72,75 @@ function touchStart(event, track) {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
/**
|
|
75
|
-
* Handles the touch end event.
|
|
75
|
+
* Handles the touch end event and triggers a swipe event if the criteria are met.
|
|
76
76
|
*
|
|
77
|
-
* @param {Event}
|
|
78
|
-
* @param {HTMLElement} node
|
|
79
|
-
* @param {
|
|
80
|
-
* @param {
|
|
77
|
+
* @param {Event} event - The event object representing the touch or mouse event.
|
|
78
|
+
* @param {HTMLElement} node - The HTML element on which the swipe event will be dispatched.
|
|
79
|
+
* @param {object} options - Configuration options for determining swipe behavior.
|
|
80
|
+
* @param {object} track - An object tracking the start point and time of the touch or swipe action.
|
|
81
81
|
*/
|
|
82
82
|
function touchEnd(event, node, options, track) {
|
|
83
|
+
const { distance, duration } = getTouchMetrics(event, track)
|
|
84
|
+
if (!isSwipeFastEnough(distance, duration, options.minSpeed)) return
|
|
85
|
+
|
|
86
|
+
const swipeDetails = getSwipeDetails(distance, options)
|
|
87
|
+
if (!swipeDetails.isValid) return
|
|
88
|
+
node.dispatchEvent(new CustomEvent(`swipe${swipeDetails.direction}`))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Calculates and returns the distance and duration of the swipe.
|
|
93
|
+
*
|
|
94
|
+
* @param {Event} event - The event object that initiated the touchEnd.
|
|
95
|
+
* @param {object} track - The tracking object holding the start of the touch action.
|
|
96
|
+
* @returns {{distance: {x: number, y: number}, duration: number}} The distance swiped (x and y) and the duration of the swipe.
|
|
97
|
+
*/
|
|
98
|
+
function getTouchMetrics(event, track) {
|
|
83
99
|
const touch = event.changedTouches ? event.changedTouches[0] : event
|
|
84
100
|
const distX = touch.clientX - track.startX
|
|
85
101
|
const distY = touch.clientY - track.startY
|
|
86
102
|
const duration = (new Date().getTime() - track.startTime) / 1000
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (speed <= options.minSpeed) return
|
|
90
|
-
|
|
91
|
-
const isHorizontalSwipe = options.horizontal && Math.abs(distX) >= options.threshold
|
|
92
|
-
const isVerticalSwipe = options.vertical && Math.abs(distY) >= options.threshold
|
|
93
|
-
|
|
94
|
-
if (!isHorizontalSwipe && !isVerticalSwipe) return
|
|
103
|
+
return { distance: { x: distX, y: distY }, duration }
|
|
104
|
+
}
|
|
95
105
|
|
|
96
|
-
|
|
97
|
-
|
|
106
|
+
/**
|
|
107
|
+
* Checks if the swipe was fast enough according to the minimum speed requirement.
|
|
108
|
+
*
|
|
109
|
+
* @param {{x: number, y: number}} distance - The distance of the swipe action.
|
|
110
|
+
* @param {number} duration - The duration of the swipe action in seconds.
|
|
111
|
+
* @param {number} minSpeed - The minimum speed threshold for the swipe action.
|
|
112
|
+
* @returns {boolean} True if the swipe is fast enough, otherwise false.
|
|
113
|
+
*/
|
|
114
|
+
function isSwipeFastEnough(distance, duration, minSpeed) {
|
|
115
|
+
const speed = Math.max(Math.abs(distance.x), Math.abs(distance.y)) / duration
|
|
116
|
+
return speed > minSpeed
|
|
98
117
|
}
|
|
99
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Determines swipe validity and direction based on horizontal/vertical preferences and thresholds.
|
|
121
|
+
*
|
|
122
|
+
* @param {{x: number, y: number}} distance - The distance of the swipe.
|
|
123
|
+
* @param {object} options - Configuration options such as direction preferences and thresholds.
|
|
124
|
+
* @returns {{isValid: boolean, direction?: string}} Object indicating whether the swipe is valid, and if so, its direction.
|
|
125
|
+
*/
|
|
126
|
+
function getSwipeDetails(distance, options) {
|
|
127
|
+
const isHorizontalSwipe = options.horizontal && Math.abs(distance.x) >= options.threshold
|
|
128
|
+
const isVerticalSwipe = options.vertical && Math.abs(distance.y) >= options.threshold
|
|
129
|
+
if (isHorizontalSwipe || isVerticalSwipe) {
|
|
130
|
+
return {
|
|
131
|
+
isValid: true,
|
|
132
|
+
direction: getSwipeDirection(distance.x, distance.y)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return { isValid: false }
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Returns the swipe direction based on the distance in the x and y axis.
|
|
139
|
+
*
|
|
140
|
+
* @param {number} distX - The distance in the x axis.
|
|
141
|
+
* @param {number} distY - The distance in the y axis.
|
|
142
|
+
* @returns {string} The swipe direction.
|
|
143
|
+
*/
|
|
100
144
|
function getSwipeDirection(distX, distY) {
|
|
101
145
|
if (Math.abs(distX) > Math.abs(distY)) {
|
|
102
146
|
return distX > 0 ? 'Right' : 'Left'
|
package/src/switchable.js
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import { removeListeners, setupListeners } from './lib'
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* A switchable action that allows the user to cycle through a list of options
|
|
5
|
+
*
|
|
6
|
+
* @param {HTMLElement} node
|
|
7
|
+
* @param {Object} data
|
|
8
|
+
*/
|
|
3
9
|
export function switchable(node, data) {
|
|
4
10
|
let index = 0
|
|
5
11
|
let { value, options, disabled } = data
|
|
6
12
|
|
|
7
|
-
const update = (
|
|
8
|
-
value =
|
|
9
|
-
options =
|
|
10
|
-
disabled =
|
|
13
|
+
const update = (input) => {
|
|
14
|
+
value = input.value === null || input.value === undefined ? options[0] : input.value
|
|
15
|
+
options = input.options
|
|
16
|
+
disabled = input.disabled
|
|
11
17
|
index = options.indexOf(value)
|
|
12
18
|
}
|
|
13
19
|
|
|
@@ -17,6 +23,22 @@ export function switchable(node, data) {
|
|
|
17
23
|
node.dispatchEvent(new CustomEvent('change', { detail: value }))
|
|
18
24
|
}
|
|
19
25
|
|
|
26
|
+
const listeners = getEventHandlers(options, toggle)
|
|
27
|
+
|
|
28
|
+
update(data)
|
|
29
|
+
setupListeners(node, listeners, { enabled: !disabled })
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
update,
|
|
33
|
+
destroy: () => removeListeners(node, listeners)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Returns a keydown handler for the switchable component
|
|
38
|
+
*
|
|
39
|
+
* @param {Object} options
|
|
40
|
+
*/
|
|
41
|
+
function getEventHandlers(options, toggle) {
|
|
20
42
|
const keydown = (e) => {
|
|
21
43
|
if ([' ', 'Enter', 'ArrowRight', 'ArrowLeft'].includes(e.key)) {
|
|
22
44
|
e.preventDefault()
|
|
@@ -25,16 +47,6 @@ export function switchable(node, data) {
|
|
|
25
47
|
toggle(e.key === 'ArrowLeft' ? options.length - 1 : 1)
|
|
26
48
|
}
|
|
27
49
|
}
|
|
28
|
-
const listeners = {
|
|
29
|
-
click: () => toggle(1),
|
|
30
|
-
keydown
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
update(data)
|
|
34
|
-
setupListeners(node, listeners, { enabled: !disabled })
|
|
35
50
|
|
|
36
|
-
return {
|
|
37
|
-
update,
|
|
38
|
-
destroy: () => removeListeners(node, listeners)
|
|
39
|
-
}
|
|
51
|
+
return { keydown, click: () => toggle(1) }
|
|
40
52
|
}
|
package/src/traversable.js
CHANGED
|
@@ -1,103 +1,396 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
// handleItemClick,
|
|
8
|
-
EventManager
|
|
9
|
-
} from './lib'
|
|
10
|
-
|
|
11
|
-
const defaultOptions = {
|
|
1
|
+
import { has } from 'ramda'
|
|
2
|
+
import { EventManager } from './lib'
|
|
3
|
+
const defaultConfig = {
|
|
4
|
+
allowDrag: false,
|
|
5
|
+
allowDrop: false,
|
|
6
|
+
pageSize: 10,
|
|
12
7
|
horizontal: false,
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
vertical: true,
|
|
9
|
+
multiselect: false
|
|
15
10
|
}
|
|
16
11
|
|
|
17
12
|
/**
|
|
18
|
-
*
|
|
13
|
+
* A svelte action to add keyboard navigation to a list/tree/grid
|
|
19
14
|
*
|
|
20
|
-
* @param {HTMLElement}
|
|
21
|
-
* @param {
|
|
22
|
-
* @
|
|
15
|
+
* @param {HTMLElement} root - The DOM root node to add the action to
|
|
16
|
+
* @param {Object} config - The configuration object
|
|
17
|
+
* @param {Object} config.store - The store object with navigation methods
|
|
18
|
+
* @param {Object} config.options - The configuration options
|
|
19
|
+
* @param {number} config.options.pageSize - The number of items to move on page up/down
|
|
20
|
+
* @param {boolean} config.options.horizontal - The orientation of the list/tree
|
|
21
|
+
* @param {boolean} config.options.vertical - The orientation of the list/tree
|
|
23
22
|
*/
|
|
24
|
-
export function traversable(
|
|
25
|
-
|
|
26
|
-
const manager = EventManager(
|
|
23
|
+
export function traversable(root, config) {
|
|
24
|
+
let store = config.store
|
|
25
|
+
const manager = EventManager(root, {})
|
|
27
26
|
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Update the event handlers based on the configuration
|
|
29
|
+
* @param {Object} config - The configuration object
|
|
30
|
+
*/
|
|
31
|
+
function update(config) {
|
|
32
|
+
store = config.store
|
|
33
|
+
const options = { ...defaultConfig, ...config.options }
|
|
30
34
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
current = result
|
|
35
|
-
checkAndEmit('move')
|
|
35
|
+
const listeners = {
|
|
36
|
+
keydown: getKeydownHandler(store, options, root),
|
|
37
|
+
click: getClickHandler(store, options)
|
|
36
38
|
}
|
|
39
|
+
if (options.allowDrag) listeners.dragstart = getDragStartHandler(store)
|
|
40
|
+
if (options.allowDrop) {
|
|
41
|
+
listeners.dragover = getDragOverHandler(store)
|
|
42
|
+
listeners.drop = getDropHandler(store)
|
|
43
|
+
}
|
|
44
|
+
manager.update(listeners)
|
|
37
45
|
}
|
|
38
46
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Cleanup action on destroy
|
|
49
|
+
*/
|
|
50
|
+
function destroy() {
|
|
51
|
+
manager.reset()
|
|
52
|
+
// store.onNavigate(null)
|
|
43
53
|
}
|
|
44
54
|
|
|
55
|
+
update(config)
|
|
56
|
+
|
|
57
|
+
return { destroy, update }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get a map of actions for various key combinations
|
|
62
|
+
*
|
|
63
|
+
* @param {Object} store - The store object with navigation methods
|
|
64
|
+
* @param {number} pageSize - The number of items to move on page up/down
|
|
65
|
+
*/
|
|
66
|
+
function getKeyHandlers(store, options) {
|
|
67
|
+
const { pageSize, horizontal, vertical } = options
|
|
68
|
+
const isGrid = horizontal && vertical
|
|
69
|
+
const arrowActions = isGrid
|
|
70
|
+
? getArrowKeyActionsForGrid(store)
|
|
71
|
+
: getArrowKeyActions(store, horizontal)
|
|
72
|
+
|
|
45
73
|
const actions = {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
74
|
+
...arrowActions,
|
|
75
|
+
PageUp: () => store.moveByOffset(-pageSize),
|
|
76
|
+
PageDown: () => store.moveByOffset(pageSize),
|
|
77
|
+
Home: () => store.moveFirst(),
|
|
78
|
+
End: () => store.moveLast(),
|
|
79
|
+
Enter: () => store.select(),
|
|
80
|
+
Escape: () => store.escape(),
|
|
81
|
+
' ': () => store.select()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const modifierActions = {
|
|
85
|
+
ctrl: getMetaKeyActions(store, horizontal),
|
|
86
|
+
meta: getMetaKeyActions(store, horizontal),
|
|
87
|
+
shift: isGrid ? getShiftKeyActionsForGrid(store) : getShiftKeyActions(store, horizontal)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return { actions, modifierActions }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Get action handlers based on direction
|
|
95
|
+
*
|
|
96
|
+
* @param {Object} store - The store object with navigation methods
|
|
97
|
+
* @param {boolean} horizontal - if the content is navigable horizontally
|
|
98
|
+
*/
|
|
99
|
+
function getArrowKeyActions(store, horizontal = false) {
|
|
100
|
+
if (horizontal) {
|
|
101
|
+
return {
|
|
102
|
+
ArrowUp: () => store.collapse(),
|
|
103
|
+
ArrowDown: () => store.expand(),
|
|
104
|
+
ArrowRight: () => store.moveByOffset(1),
|
|
105
|
+
ArrowLeft: () => store.moveByOffset(-1)
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
return {
|
|
109
|
+
ArrowUp: () => store.moveByOffset(-1),
|
|
110
|
+
ArrowDown: () => store.moveByOffset(1),
|
|
111
|
+
ArrowRight: () => store.expand(),
|
|
112
|
+
ArrowLeft: () => store.collapse()
|
|
113
|
+
}
|
|
52
114
|
}
|
|
115
|
+
}
|
|
53
116
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
117
|
+
/**
|
|
118
|
+
* Get the handler function for the keydown event
|
|
119
|
+
*
|
|
120
|
+
* @param {Object} store - The store object with navigation methods
|
|
121
|
+
* @param {Object} options - The configuration options
|
|
122
|
+
*/
|
|
123
|
+
function getClickHandler(store, options) {
|
|
124
|
+
const { multiselect = false } = options
|
|
125
|
+
|
|
126
|
+
function handleClick(event) {
|
|
127
|
+
const modifiers = identifyModifiers(event)
|
|
128
|
+
const indexPath = getTargetIndex(event)
|
|
129
|
+
|
|
130
|
+
if (!indexPath) return
|
|
131
|
+
event.stopPropagation()
|
|
132
|
+
|
|
133
|
+
if (isToggleStateIcon(event.target)) {
|
|
134
|
+
store.toggleExpansion(indexPath)
|
|
135
|
+
} else {
|
|
136
|
+
if (multiselect) {
|
|
137
|
+
handleMultiSelect(store, indexPath, modifiers)
|
|
138
|
+
} else {
|
|
139
|
+
store.moveTo(indexPath)
|
|
140
|
+
store.select(indexPath)
|
|
141
|
+
}
|
|
57
142
|
}
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
143
|
+
// dispatchEvents(event.target, store)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return handleClick
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Get a function to handle the dragstart event
|
|
150
|
+
*
|
|
151
|
+
* @param {Object} store - The store object with navigation methods
|
|
152
|
+
*/
|
|
153
|
+
function getDragStartHandler(store) {
|
|
154
|
+
function handleDragStart(event) {
|
|
155
|
+
const index = getTargetIndex(event)
|
|
156
|
+
if (index) store.dragStart(index)
|
|
157
|
+
}
|
|
158
|
+
return handleDragStart
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Get a function to handle the dragover event
|
|
163
|
+
*
|
|
164
|
+
* @param {Object} store - The store object with navigation methods
|
|
165
|
+
*/
|
|
166
|
+
function getDragOverHandler(store) {
|
|
167
|
+
function handleDragOver(event) {
|
|
168
|
+
const index = getTargetIndex(event)
|
|
169
|
+
if (index) store.dragOver(index)
|
|
170
|
+
}
|
|
171
|
+
return handleDragOver
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Get a function to handle the drop event
|
|
176
|
+
*
|
|
177
|
+
* @param {Object} store - The store object with navigation methods
|
|
178
|
+
*/
|
|
179
|
+
function getDropHandler(store) {
|
|
180
|
+
function handleDrop(event) {
|
|
181
|
+
const index = getTargetIndex(event)
|
|
182
|
+
if (index) store.dropOver(index)
|
|
183
|
+
}
|
|
184
|
+
return handleDrop
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Handle multi-select based on the modifier keys pressed
|
|
188
|
+
*
|
|
189
|
+
* @param {Object} store - The store object with navigation methods
|
|
190
|
+
* @param {number[]} index - The index path of the item to select
|
|
191
|
+
* @param {string[]} modifier - The modifier keys pressed
|
|
192
|
+
*/
|
|
193
|
+
function handleMultiSelect(store, index, modifier) {
|
|
194
|
+
if (modifier.includes('shift')) {
|
|
195
|
+
store.selectRange(index)
|
|
196
|
+
} else if (modifier.includes('ctrl') || modifier.includes('meta')) {
|
|
197
|
+
store.toggleSelection(index)
|
|
198
|
+
} else {
|
|
199
|
+
store.select(index)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Get the keydown event handler
|
|
204
|
+
*
|
|
205
|
+
* @param {Object} store - The store object with navigation methods
|
|
206
|
+
* @param {Object} options - The configuration options
|
|
207
|
+
* @param {HTMLElement} root - The root element to add the event listener to
|
|
208
|
+
*/
|
|
209
|
+
function getKeydownHandler(store, options, root) {
|
|
210
|
+
const handlers = getKeyHandlers(store, options)
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Use the keyboard event map to handle the keydown event
|
|
214
|
+
*
|
|
215
|
+
* @param {KeyboardEvent} event - The keyboard event
|
|
216
|
+
*/
|
|
217
|
+
function handleKeydown(event) {
|
|
218
|
+
const action = getAction(event, handlers)
|
|
219
|
+
if (action) {
|
|
220
|
+
event.preventDefault()
|
|
221
|
+
action()
|
|
222
|
+
scrollIntoView(root, store)
|
|
223
|
+
dispatchEvents(root, store)
|
|
79
224
|
}
|
|
80
|
-
// current = handleValueChange(element, data, content, current)
|
|
81
225
|
}
|
|
82
226
|
|
|
83
|
-
|
|
227
|
+
return handleKeydown
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Get the action for the keydown event
|
|
232
|
+
*
|
|
233
|
+
* @param {KeyboardEvent} event - The keyboard event
|
|
234
|
+
* @param {Object} handlers - The key handlers object
|
|
235
|
+
*/
|
|
236
|
+
function getAction(event, handlers) {
|
|
237
|
+
const key = event.key.length === 1 ? event.key.toUpperCase() : event.key
|
|
238
|
+
const modifier = identifyModifiers(event).join('-')
|
|
239
|
+
if (modifier.length === 0) return handlers.actions[key]
|
|
240
|
+
|
|
241
|
+
if (has(modifier, handlers.modifierActions)) {
|
|
242
|
+
return handlers.modifierActions[modifier][key]
|
|
243
|
+
}
|
|
244
|
+
return null
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Identify modifier keys pressed in the event
|
|
249
|
+
*
|
|
250
|
+
* @param {KeyboardEvent} event - The keyboard event
|
|
251
|
+
*/
|
|
252
|
+
function identifyModifiers(event) {
|
|
253
|
+
const modifiers = []
|
|
254
|
+
|
|
255
|
+
if (event.ctrlKey) modifiers.push('ctrl')
|
|
256
|
+
if (event.shiftKey) modifiers.push('shift')
|
|
257
|
+
if (event.metaKey) modifiers.push('meta')
|
|
258
|
+
|
|
259
|
+
return modifiers
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Get the meta key actions for a list/tree
|
|
264
|
+
*
|
|
265
|
+
* @param {Object} store - The store object with navigation methods
|
|
266
|
+
* @param {boolean} horizontal - The orientation of the list/tree
|
|
267
|
+
*/
|
|
268
|
+
function getMetaKeyActions(store, horizontal = false) {
|
|
269
|
+
const actions = {
|
|
270
|
+
X: () => store.cut(),
|
|
271
|
+
C: () => store.copy(),
|
|
272
|
+
V: () => store.paste(),
|
|
273
|
+
A: () => store.selectAll(),
|
|
274
|
+
D: () => store.selectNone(),
|
|
275
|
+
I: () => store.selectInvert(),
|
|
276
|
+
Z: () => store.undo(),
|
|
277
|
+
Y: () => store.redo()
|
|
278
|
+
}
|
|
279
|
+
const horizontalActions = {
|
|
280
|
+
ArrowRight: () => store.moveLast(),
|
|
281
|
+
ArrowLeft: () => store.moveFirst()
|
|
282
|
+
}
|
|
283
|
+
const verticalActions = {
|
|
284
|
+
ArrowUp: () => store.moveFirst(),
|
|
285
|
+
ArrowDown: () => store.moveLast()
|
|
286
|
+
}
|
|
287
|
+
const arrowActions = horizontal ? horizontalActions : verticalActions
|
|
288
|
+
|
|
289
|
+
return { ...actions, ...arrowActions }
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Get the shift key actions for a list
|
|
294
|
+
*
|
|
295
|
+
* @param {Object} store - The store object with navigation methods
|
|
296
|
+
* @param {boolean} horizontal - The orientation of the list/tree
|
|
297
|
+
*/
|
|
298
|
+
function getShiftKeyActions(store, horizontal = false) {
|
|
299
|
+
const actions = {
|
|
300
|
+
Home: () => store.selectRange(-Infinity),
|
|
301
|
+
End: () => store.selectRange(Infinity)
|
|
302
|
+
}
|
|
303
|
+
const horizontalActions = {
|
|
304
|
+
ArrowRight: () => store.selectRange(1),
|
|
305
|
+
ArrowLeft: () => store.selectRange(-1)
|
|
306
|
+
}
|
|
307
|
+
const verticalActions = {
|
|
308
|
+
ArrowUp: () => store.selectRange(-1),
|
|
309
|
+
ArrowDown: () => store.selectRange(1)
|
|
310
|
+
}
|
|
311
|
+
const arrowActions = horizontal ? horizontalActions : verticalActions
|
|
312
|
+
|
|
313
|
+
return { ...actions, ...arrowActions }
|
|
314
|
+
}
|
|
84
315
|
|
|
316
|
+
/**
|
|
317
|
+
* Get the arrow key actions for a grid
|
|
318
|
+
*
|
|
319
|
+
* @param {Object} store - The store object with navigation methods
|
|
320
|
+
* @returns {Object} - The map of actions
|
|
321
|
+
*/
|
|
322
|
+
function getArrowKeyActionsForGrid(store) {
|
|
85
323
|
return {
|
|
86
|
-
|
|
87
|
-
|
|
324
|
+
ArrowUp: () => store.moveByOffset(-1),
|
|
325
|
+
ArrowDown: () => store.moveByOffset(1),
|
|
326
|
+
ArrowRight: () => store.moveByOffset(0, 1),
|
|
327
|
+
ArrowLeft: () => store.moveByOffset(0, -1),
|
|
328
|
+
Home: () => store.moveByOffset(-Infinity, -Infinity),
|
|
329
|
+
End: () => store.moveByOffset(Infinity, Infinity)
|
|
88
330
|
}
|
|
89
331
|
}
|
|
90
332
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
333
|
+
/**
|
|
334
|
+
* Get the shift key actions for a grid
|
|
335
|
+
*
|
|
336
|
+
* @param {Object} store - The store object with navigation methods
|
|
337
|
+
* @returns {Object} - The map of actions
|
|
338
|
+
*/
|
|
339
|
+
function getShiftKeyActionsForGrid(store) {
|
|
340
|
+
return {
|
|
341
|
+
ArrowUp: () => store.selectRange(-1),
|
|
342
|
+
ArrowDown: () => store.selectRange(1),
|
|
343
|
+
ArrowRight: () => store.selectRange(0, 1),
|
|
344
|
+
ArrowLeft: () => store.selectRange(0, -1),
|
|
345
|
+
Home: () => store.selectRange(0, -Infinity),
|
|
346
|
+
End: () => store.selectRange(0, Infinity)
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Identify if an html element is a toggle state icon
|
|
352
|
+
* A toggle state icon element tag is ICON and has a data-state attribute value of 'opened' or 'closed'
|
|
353
|
+
*
|
|
354
|
+
* @param {HTMLElement} element - The html element to check
|
|
355
|
+
*/
|
|
356
|
+
function isToggleStateIcon(element) {
|
|
357
|
+
return (
|
|
358
|
+
element.tagName === 'ICON' && ['opened', 'closed'].includes(element.getAttribute('data-state'))
|
|
359
|
+
)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Get the index of the target element
|
|
364
|
+
*
|
|
365
|
+
* @param {MouseEvent} event - The mouse event
|
|
366
|
+
*/
|
|
367
|
+
function getTargetIndex(event) {
|
|
368
|
+
const target = event.target.closest('[data-index]')
|
|
369
|
+
if (target) return target.getAttribute('data-index').split('-').map(Number)
|
|
370
|
+
|
|
371
|
+
return null
|
|
372
|
+
}
|
|
98
373
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
374
|
+
/**
|
|
375
|
+
* Make the current item visible in the view
|
|
376
|
+
*
|
|
377
|
+
* @param {HTMLElement} root - The root element which contains the items
|
|
378
|
+
* @param {Object} store - The item to make visible
|
|
379
|
+
*/
|
|
380
|
+
function scrollIntoView(root, store) {
|
|
381
|
+
const item = store.currentItem()
|
|
382
|
+
const dataIndex = item.indexPath.join('-')
|
|
383
|
+
const node = root.querySelector(`[data-index="${dataIndex}"]`)
|
|
384
|
+
if (node) node.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Dispatch custom events based on the state changes
|
|
389
|
+
*
|
|
390
|
+
* @param {HTMLElement} root - The root element to dispatch the events from
|
|
391
|
+
* @param {Object} store - The store object with navigation methods
|
|
392
|
+
*/
|
|
393
|
+
function dispatchEvents(root, store) {
|
|
394
|
+
const events = store.getEvents()
|
|
395
|
+
events.forEach((event, detail) => root.dispatchEvent(new CustomEvent(event, { detail })))
|
|
396
|
+
}
|