@rokkit/actions 1.0.0-next.37 → 1.0.0-next.39
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 +2 -2
- package/src/hierarchy.js +1 -44
- package/src/lib/index.js +1 -0
- package/src/lib/internal.js +89 -0
- package/src/navigable.js +30 -32
- package/src/navigator.js +31 -22
- package/src/pannable.js +21 -18
- package/src/swipeable.js +66 -52
- package/src/traversable.js +90 -0
- package/src/types.js +58 -0
- package/src/utils.js +24 -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.39",
|
|
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",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"validators": "latest",
|
|
24
24
|
"vite": "^4.4.7",
|
|
25
25
|
"vitest": "~0.33.0",
|
|
26
|
-
"shared-config": "1.0.0-next.
|
|
26
|
+
"shared-config": "1.0.0-next.39"
|
|
27
27
|
},
|
|
28
28
|
"files": [
|
|
29
29
|
"src/**/*.js",
|
package/src/hierarchy.js
CHANGED
|
@@ -1,47 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Check if the current item is a parent
|
|
3
|
-
*
|
|
4
|
-
* @param {*} item
|
|
5
|
-
* @param {import('@rokkit/core').FieldMapping} fields
|
|
6
|
-
* @returns {boolean}
|
|
7
|
-
*/
|
|
8
|
-
export function hasChildren(item, fields) {
|
|
9
|
-
return (
|
|
10
|
-
typeof item === 'object' &&
|
|
11
|
-
fields.children in item &&
|
|
12
|
-
Array.isArray(item[fields.children])
|
|
13
|
-
)
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Check if the current item is a parent and is expanded
|
|
18
|
-
*
|
|
19
|
-
* @param {*} item
|
|
20
|
-
* @param {import('@rokkit/core').FieldMapping} fields
|
|
21
|
-
* @returns {boolean}
|
|
22
|
-
*/
|
|
23
|
-
export function isExpanded(item, fields) {
|
|
24
|
-
if (item == null) return false
|
|
25
|
-
if (!hasChildren(item, fields)) return false
|
|
26
|
-
if (fields.isOpen in item) {
|
|
27
|
-
return item[fields.isOpen]
|
|
28
|
-
}
|
|
29
|
-
return false
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Verify if at least one item has children
|
|
34
|
-
*
|
|
35
|
-
* @param {Array<*>} items
|
|
36
|
-
* @param {import('@rokkit/core').FieldMapping} fields
|
|
37
|
-
* @returns {boolean}
|
|
38
|
-
*/
|
|
39
|
-
export function isNested(items, fields) {
|
|
40
|
-
for (let i = 0; i < items.length; i++) {
|
|
41
|
-
if (hasChildren(items[i], fields)) return true
|
|
42
|
-
}
|
|
43
|
-
return false
|
|
44
|
-
}
|
|
1
|
+
import { isExpanded } from '@rokkit/core'
|
|
45
2
|
|
|
46
3
|
/**
|
|
47
4
|
* Navigate to last visible child in the hirarchy starting with the provided path
|
package/src/lib/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './internal'
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { compact } from '@rokkit/core'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Emits a custom event with the given data.
|
|
5
|
+
*
|
|
6
|
+
* @param {HTMLElement} element
|
|
7
|
+
* @param {string} event
|
|
8
|
+
* @param {*} data
|
|
9
|
+
* @returns {void}
|
|
10
|
+
*/
|
|
11
|
+
export function emit(element, event, data) {
|
|
12
|
+
element.dispatchEvent(new CustomEvent(event, { detail: data }))
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Maps keyboard events to actions based on the given handlers and options.
|
|
17
|
+
*
|
|
18
|
+
* @param {import('../types').ActionHandlers} handlers
|
|
19
|
+
* @param {import('../types').NavigationOptions} options
|
|
20
|
+
* @returns {import('../types').KeyboardActions}
|
|
21
|
+
*/
|
|
22
|
+
export function mapKeyboardEventsToActions(handlers, options) {
|
|
23
|
+
const { next, previous, select, escape } = handlers
|
|
24
|
+
const { horizontal, nested } = {
|
|
25
|
+
horizontal: false,
|
|
26
|
+
nested: false,
|
|
27
|
+
...options
|
|
28
|
+
}
|
|
29
|
+
let expand = nested ? handlers.expand : null
|
|
30
|
+
let collapse = nested ? handlers.collapse : null
|
|
31
|
+
|
|
32
|
+
return compact({
|
|
33
|
+
ArrowDown: horizontal ? expand : next,
|
|
34
|
+
ArrowUp: horizontal ? collapse : previous,
|
|
35
|
+
ArrowRight: horizontal ? next : expand,
|
|
36
|
+
ArrowLeft: horizontal ? previous : collapse,
|
|
37
|
+
Enter: select,
|
|
38
|
+
Escape: escape,
|
|
39
|
+
' ': select
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Finds the closest ancestor of the given element that has the given attribute.
|
|
45
|
+
*
|
|
46
|
+
* @param {HTMLElement} element
|
|
47
|
+
* @param {string} attribute
|
|
48
|
+
* @returns {HTMLElement|null}
|
|
49
|
+
*/
|
|
50
|
+
export function getClosestAncestorWithAttribute(element, attribute) {
|
|
51
|
+
if (!element) return null
|
|
52
|
+
if (element.getAttribute(attribute)) return element
|
|
53
|
+
return getClosestAncestorWithAttribute(element.parentElement, attribute)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Sets up event handlers based on the given options.
|
|
58
|
+
* Returns whether or not the event handlers are listening.
|
|
59
|
+
*
|
|
60
|
+
* @param {HTMLElement} element
|
|
61
|
+
* @param {import('../types').EventHandlers} listeners
|
|
62
|
+
* @param {import('../types').TraversableOptions} options
|
|
63
|
+
* @returns {void}
|
|
64
|
+
*/
|
|
65
|
+
export function setupListeners(element, listeners, options) {
|
|
66
|
+
const { enabled } = { enabled: true, ...options }
|
|
67
|
+
|
|
68
|
+
if (enabled) {
|
|
69
|
+
Object.entries(listeners).forEach(([event, listener]) =>
|
|
70
|
+
element.addEventListener(event, listener)
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Removes event handlers based on the given options.
|
|
77
|
+
* Returns whether or not the event handlers are listening.
|
|
78
|
+
*
|
|
79
|
+
* @param {HTMLElement} element
|
|
80
|
+
* @param {import('../types').EventHandlers} listeners
|
|
81
|
+
* @returns {void}
|
|
82
|
+
*/
|
|
83
|
+
export function removeListeners(element, listeners) {
|
|
84
|
+
if (listeners) {
|
|
85
|
+
Object.entries(listeners).forEach(([event, listener]) => {
|
|
86
|
+
element.removeEventListener(event, listener)
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
}
|
package/src/navigable.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { handleAction, getKeyboardActions } from './utils'
|
|
2
|
+
|
|
3
|
+
const defaultOptions = { horizontal: true, nested: false, enabled: true }
|
|
1
4
|
/**
|
|
2
5
|
* A svelte action function that captures keyboard evvents and emits event for corresponding movements.
|
|
3
6
|
*
|
|
@@ -5,45 +8,40 @@
|
|
|
5
8
|
* @param {import('./types').NavigableOptions} options
|
|
6
9
|
* @returns {import('./types').SvelteActionReturn}
|
|
7
10
|
*/
|
|
8
|
-
export function navigable(
|
|
9
|
-
|
|
10
|
-
{ horizontal = true, nested = false, enabled = true } = {}
|
|
11
|
-
) {
|
|
12
|
-
if (!enabled) return { destroy() {} }
|
|
13
|
-
const previous = () => node.dispatchEvent(new CustomEvent('previous'))
|
|
14
|
-
const next = () => node.dispatchEvent(new CustomEvent('next'))
|
|
15
|
-
const collapse = () => node.dispatchEvent(new CustomEvent('collapse'))
|
|
16
|
-
const expand = () => node.dispatchEvent(new CustomEvent('expand'))
|
|
17
|
-
const select = () => node.dispatchEvent(new CustomEvent('select'))
|
|
11
|
+
export function navigable(node, options) {
|
|
12
|
+
options = { ...defaultOptions, ...options }
|
|
18
13
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
:
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
: {}
|
|
27
|
-
const actions = {
|
|
28
|
-
Enter: select,
|
|
29
|
-
' ': select,
|
|
30
|
-
...movement,
|
|
31
|
-
...change
|
|
14
|
+
let listening = false
|
|
15
|
+
const handlers = {
|
|
16
|
+
previous: () => node.dispatchEvent(new CustomEvent('previous')),
|
|
17
|
+
next: () => node.dispatchEvent(new CustomEvent('next')),
|
|
18
|
+
collapse: () => node.dispatchEvent(new CustomEvent('collapse')),
|
|
19
|
+
expand: () => node.dispatchEvent(new CustomEvent('expand')),
|
|
20
|
+
select: () => node.dispatchEvent(new CustomEvent('select'))
|
|
32
21
|
}
|
|
33
22
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
23
|
+
let actions = {} //getKeyboardActions(node, { horizontal, nested })
|
|
24
|
+
const handleKeydown = (event) => handleAction(actions, event)
|
|
25
|
+
|
|
26
|
+
function updateListeners(options) {
|
|
27
|
+
if (options.enabled) actions = getKeyboardActions(node, options, handlers)
|
|
28
|
+
|
|
29
|
+
if (options.enabled && !listening)
|
|
30
|
+
node.addEventListener('keydown', handleKeydown)
|
|
31
|
+
else if (!options.enabled && listening)
|
|
32
|
+
node.removeEventListener('keydown', handleKeydown)
|
|
33
|
+
listening = options.enabled
|
|
40
34
|
}
|
|
41
35
|
|
|
42
|
-
|
|
36
|
+
updateListeners(options)
|
|
43
37
|
|
|
44
38
|
return {
|
|
45
|
-
|
|
46
|
-
|
|
39
|
+
update: (config) => {
|
|
40
|
+
options = { ...options, ...config }
|
|
41
|
+
updateListeners(options)
|
|
42
|
+
},
|
|
43
|
+
destroy: () => {
|
|
44
|
+
updateListeners({ enabled: false })
|
|
47
45
|
}
|
|
48
46
|
}
|
|
49
47
|
}
|
package/src/navigator.js
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
import { handleAction } from './utils'
|
|
2
|
+
import { isNested, hasChildren, isExpanded } from '@rokkit/core'
|
|
2
3
|
import {
|
|
3
4
|
moveNext,
|
|
4
5
|
movePrevious,
|
|
5
|
-
isNested,
|
|
6
|
-
hasChildren,
|
|
7
6
|
pathFromIndices,
|
|
8
7
|
indicesFromPath,
|
|
9
|
-
getCurrentNode
|
|
10
|
-
isExpanded
|
|
8
|
+
getCurrentNode
|
|
11
9
|
} from './hierarchy'
|
|
12
10
|
|
|
13
11
|
/**
|
|
@@ -80,27 +78,14 @@ export function navigator(element, options) {
|
|
|
80
78
|
emit('expand', element, indicesFromPath(path), currentNode)
|
|
81
79
|
}
|
|
82
80
|
}
|
|
81
|
+
const handlers = { next, previous, select, collapse, expand }
|
|
83
82
|
|
|
84
83
|
update(options)
|
|
85
84
|
|
|
86
85
|
const nested = isNested(items, fields)
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const states = !nested
|
|
91
|
-
? {}
|
|
92
|
-
: vertical
|
|
93
|
-
? { ArrowRight: expand, ArrowLeft: collapse }
|
|
94
|
-
: { ArrowDown: expand, ArrowUp: collapse }
|
|
95
|
-
const actions = { ...movement, Enter: select, ...states }
|
|
96
|
-
|
|
97
|
-
const handleKeyDown = (event) => {
|
|
98
|
-
if (actions[event.key]) {
|
|
99
|
-
event.preventDefault()
|
|
100
|
-
event.stopPropagation()
|
|
101
|
-
actions[event.key]()
|
|
102
|
-
}
|
|
103
|
-
}
|
|
86
|
+
const actions = mapKeyboardEventsToActions(vertical, nested, handlers)
|
|
87
|
+
|
|
88
|
+
const handleKeyDown = (event) => handleAction(actions, event)
|
|
104
89
|
|
|
105
90
|
const handleClick = (event) => {
|
|
106
91
|
let target = findParentWithDataPath(event.target, element)
|
|
@@ -189,3 +174,27 @@ function emit(event, element, indices, node) {
|
|
|
189
174
|
})
|
|
190
175
|
)
|
|
191
176
|
}
|
|
177
|
+
|
|
178
|
+
function mapKeyboardEventsToActions(vertical, nested, handlers) {
|
|
179
|
+
let actions = { Enter: handlers.select }
|
|
180
|
+
|
|
181
|
+
if (vertical) {
|
|
182
|
+
actions = {
|
|
183
|
+
...actions,
|
|
184
|
+
...{ ArrowDown: handlers.next, ArrowUp: handlers.previous },
|
|
185
|
+
...(nested
|
|
186
|
+
? { ArrowRight: handlers.expand, ArrowLeft: handlers.collapse }
|
|
187
|
+
: {})
|
|
188
|
+
}
|
|
189
|
+
} else {
|
|
190
|
+
actions = {
|
|
191
|
+
...actions,
|
|
192
|
+
...{ ArrowRight: handlers.next, ArrowLeft: handlers.previous },
|
|
193
|
+
...(nested
|
|
194
|
+
? { ArrowDown: handlers.expand, ArrowUp: handlers.collapse }
|
|
195
|
+
: {})
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return actions
|
|
200
|
+
}
|
package/src/pannable.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { removeListeners, setupListeners } from './lib'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Handle drag and move events
|
|
3
5
|
*
|
|
@@ -7,6 +9,18 @@
|
|
|
7
9
|
export function pannable(node) {
|
|
8
10
|
let x
|
|
9
11
|
let y
|
|
12
|
+
let listeners = {
|
|
13
|
+
primary: {
|
|
14
|
+
mousedown: start,
|
|
15
|
+
touchstart: start
|
|
16
|
+
},
|
|
17
|
+
secondary: {
|
|
18
|
+
mousemove: move,
|
|
19
|
+
mouseup: stop,
|
|
20
|
+
touchmove: move,
|
|
21
|
+
touchend: stop
|
|
22
|
+
}
|
|
23
|
+
}
|
|
10
24
|
|
|
11
25
|
function track(event, name, delta = {}) {
|
|
12
26
|
x = event.clientX || event.touches[0].clientX
|
|
@@ -20,37 +34,26 @@ export function pannable(node) {
|
|
|
20
34
|
)
|
|
21
35
|
}
|
|
22
36
|
|
|
23
|
-
function
|
|
37
|
+
function start(event) {
|
|
24
38
|
track(event, 'panstart')
|
|
25
|
-
window
|
|
26
|
-
window.addEventListener('mouseup', handleMouseup)
|
|
27
|
-
window.addEventListener('touchmove', handleMousemove, { passive: false })
|
|
28
|
-
window.addEventListener('touchend', handleMouseup)
|
|
39
|
+
setupListeners(window, listeners.secondary)
|
|
29
40
|
}
|
|
30
41
|
|
|
31
|
-
function
|
|
42
|
+
function move(event) {
|
|
32
43
|
const dx = (event.clientX || event.touches[0].clientX) - x
|
|
33
44
|
const dy = (event.clientY || event.touches[0].clientY) - y
|
|
34
45
|
|
|
35
46
|
track(event, 'panmove', { dx, dy })
|
|
36
47
|
}
|
|
37
48
|
|
|
38
|
-
function
|
|
49
|
+
function stop(event) {
|
|
39
50
|
track(event, 'panend')
|
|
40
|
-
|
|
41
|
-
window.removeEventListener('mousemove', handleMousemove)
|
|
42
|
-
window.removeEventListener('mouseup', handleMouseup)
|
|
43
|
-
window.removeEventListener('touchmove', handleMousemove)
|
|
44
|
-
window.removeEventListener('touchend', handleMouseup)
|
|
51
|
+
removeListeners(window, listeners.secondary)
|
|
45
52
|
}
|
|
46
53
|
|
|
47
|
-
node
|
|
48
|
-
node.addEventListener('touchstart', handleMousedown, { passive: false })
|
|
54
|
+
setupListeners(node, listeners.primary)
|
|
49
55
|
|
|
50
56
|
return {
|
|
51
|
-
destroy()
|
|
52
|
-
node.removeEventListener('mousedown', handleMousedown)
|
|
53
|
-
node.removeEventListener('touchstart', handleMousedown)
|
|
54
|
-
}
|
|
57
|
+
destroy: () => removeListeners(node, listeners.primary)
|
|
55
58
|
}
|
|
56
59
|
}
|
package/src/swipeable.js
CHANGED
|
@@ -6,68 +6,82 @@
|
|
|
6
6
|
* @returns {import('./types').SvelteActionReturn}
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
let startX
|
|
22
|
-
let startY
|
|
23
|
-
let startTime
|
|
9
|
+
import { removeListeners, setupListeners } from './lib'
|
|
10
|
+
const defaultOptions = {
|
|
11
|
+
horizontal: true,
|
|
12
|
+
vertical: false,
|
|
13
|
+
threshold: 100,
|
|
14
|
+
enabled: true,
|
|
15
|
+
minSpeed: 300
|
|
16
|
+
}
|
|
17
|
+
export function swipeable(node, options = defaultOptions) {
|
|
18
|
+
let track = {}
|
|
19
|
+
let listeners = {}
|
|
24
20
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
startTime = new Date().getTime()
|
|
21
|
+
const updateListeners = (options) => {
|
|
22
|
+
removeListeners(node, listeners)
|
|
23
|
+
listeners = getListeners(node, options, track)
|
|
24
|
+
setupListeners(node, listeners, options)
|
|
30
25
|
}
|
|
31
26
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const distX = touch.clientX - startX
|
|
35
|
-
const distY = touch.clientY - startY
|
|
36
|
-
const duration = (new Date().getTime() - startTime) / 1000
|
|
37
|
-
const speed = Math.max(Math.abs(distX), Math.abs(distY)) / duration
|
|
27
|
+
options = { ...defaultOptions, ...options }
|
|
28
|
+
updateListeners(options)
|
|
38
29
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
30
|
+
return {
|
|
31
|
+
update: (data) => {
|
|
32
|
+
options = { ...options, ...data }
|
|
33
|
+
updateListeners(options)
|
|
34
|
+
},
|
|
35
|
+
destroy() {
|
|
36
|
+
removeListeners(node, listeners)
|
|
47
37
|
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getListeners(node, options, track) {
|
|
42
|
+
if (!options.enabled) return {}
|
|
43
|
+
|
|
44
|
+
let listeners = {
|
|
45
|
+
touchend: (e) => touchEnd(e, node, options, track),
|
|
46
|
+
touchstart: (e) => touchStart(e, track),
|
|
47
|
+
mousedown: (e) => touchStart(e, track),
|
|
48
|
+
mouseup: (e) => touchEnd(e, node, options, track)
|
|
49
|
+
}
|
|
50
|
+
return listeners
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function touchStart(event, track) {
|
|
54
|
+
const touch = event.touches ? event.touches[0] : event
|
|
55
|
+
track.startX = touch.clientX
|
|
56
|
+
track.startY = touch.clientY
|
|
57
|
+
track.startTime = new Date().getTime()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function touchEnd(event, node, options, track) {
|
|
61
|
+
const { horizontal, vertical, threshold, minSpeed } = options
|
|
62
|
+
const touch = event.changedTouches ? event.changedTouches[0] : event
|
|
63
|
+
const distX = touch.clientX - track.startX
|
|
64
|
+
const distY = touch.clientY - track.startY
|
|
65
|
+
const duration = (new Date().getTime() - track.startTime) / 1000
|
|
66
|
+
const speed = Math.max(Math.abs(distX), Math.abs(distY)) / duration
|
|
48
67
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
68
|
+
if (horizontal && speed > minSpeed) {
|
|
69
|
+
if (Math.abs(distX) > Math.abs(distY) && Math.abs(distX) >= threshold) {
|
|
70
|
+
if (distX > 0 && distX / duration > minSpeed) {
|
|
71
|
+
node.dispatchEvent(new CustomEvent('swipeRight'))
|
|
72
|
+
} else {
|
|
73
|
+
node.dispatchEvent(new CustomEvent('swipeLeft'))
|
|
56
74
|
}
|
|
57
75
|
}
|
|
58
76
|
}
|
|
59
77
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
node.removeEventListener('touchstart', touchStart)
|
|
68
|
-
node.removeEventListener('touchend', touchEnd)
|
|
69
|
-
node.removeEventListener('mousedown', touchStart)
|
|
70
|
-
node.removeEventListener('mouseup', touchEnd)
|
|
78
|
+
if (vertical && speed > minSpeed) {
|
|
79
|
+
if (Math.abs(distY) > Math.abs(distX) && Math.abs(distY) >= threshold) {
|
|
80
|
+
if (distY > 0) {
|
|
81
|
+
node.dispatchEvent(new CustomEvent('swipeDown'))
|
|
82
|
+
} else {
|
|
83
|
+
node.dispatchEvent(new CustomEvent('swipeUp'))
|
|
84
|
+
}
|
|
71
85
|
}
|
|
72
86
|
}
|
|
73
87
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getClosestAncestorWithAttribute,
|
|
3
|
+
mapKeyboardEventsToActions,
|
|
4
|
+
setupListeners,
|
|
5
|
+
removeListeners,
|
|
6
|
+
emit
|
|
7
|
+
} from './lib'
|
|
8
|
+
|
|
9
|
+
const defaultOptions = {
|
|
10
|
+
horizontal: false,
|
|
11
|
+
nested: false,
|
|
12
|
+
enabled: true
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* An action that can be used to traverse a nested list of items using keyboard and mouse.
|
|
17
|
+
*
|
|
18
|
+
* @param {HTMLElement} element
|
|
19
|
+
* @param {import('./types').TraversableOptions} data
|
|
20
|
+
* @returns
|
|
21
|
+
*/
|
|
22
|
+
export function traversable(element, data) {
|
|
23
|
+
let options = {}
|
|
24
|
+
let tracker = {}
|
|
25
|
+
let handlers
|
|
26
|
+
let actions
|
|
27
|
+
let listeners
|
|
28
|
+
|
|
29
|
+
const configure = (data) => {
|
|
30
|
+
options = { ...options, ...data }
|
|
31
|
+
|
|
32
|
+
removeListeners(element, listeners)
|
|
33
|
+
actions = getActions(element, tracker)
|
|
34
|
+
handlers = mapKeyboardEventsToActions(actions, options)
|
|
35
|
+
listeners = getListeners(handlers, actions, tracker)
|
|
36
|
+
setupListeners(element, listeners, options)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
configure({ ...defaultOptions, ...data })
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
update: configure,
|
|
43
|
+
destroy: () => removeListeners(element, listeners)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Returns the listeners for the given handlers and actions.
|
|
49
|
+
*
|
|
50
|
+
* @param {import('./types').KeyboardActions} handlers
|
|
51
|
+
* @param {import('./types').ActionHandlers} actions
|
|
52
|
+
* @param {import('./types').PositionTracker} tracker
|
|
53
|
+
*/
|
|
54
|
+
function getListeners(handlers, actions, tracker) {
|
|
55
|
+
return {
|
|
56
|
+
keydown: (event) => {
|
|
57
|
+
const action = handlers[event.key]
|
|
58
|
+
if (action) action(event)
|
|
59
|
+
},
|
|
60
|
+
click: (event) => {
|
|
61
|
+
const target = getClosestAncestorWithAttribute(event.target, 'data-index')
|
|
62
|
+
|
|
63
|
+
if (target) {
|
|
64
|
+
const index = parseInt(target.getAttribute('data-index'))
|
|
65
|
+
tracker.index = index
|
|
66
|
+
actions.select()
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
*
|
|
74
|
+
* @param {HTMLElement} element
|
|
75
|
+
* @param {import('./types').PositionTracker} tracker
|
|
76
|
+
* @returns {import('./types').ActionHandlers}
|
|
77
|
+
*/
|
|
78
|
+
function getActions(element, tracker) {
|
|
79
|
+
const actions = {
|
|
80
|
+
next: () => emit(element, 'move', tracker),
|
|
81
|
+
previous: () => emit(element, 'move', tracker),
|
|
82
|
+
select: () => emit(element, 'select', tracker),
|
|
83
|
+
escape: () => emit(element, 'escape', tracker),
|
|
84
|
+
collapse: () => emit(element, 'collapse', tracker),
|
|
85
|
+
expand: () => emit(element, 'expand', tracker)
|
|
86
|
+
}
|
|
87
|
+
return actions
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// function handleValueChange(element, options) {}
|
package/src/types.js
CHANGED
|
@@ -51,3 +51,61 @@
|
|
|
51
51
|
* @property {number} threshold - Threshold for swipe
|
|
52
52
|
* @property {number} minSpeed - Minimum speed for swipe
|
|
53
53
|
*/
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @typedef TraversableOptions
|
|
57
|
+
* @property {boolean} horizontal - Traverse horizontally
|
|
58
|
+
* @property {boolean} nested - Traverse nested items
|
|
59
|
+
* @property {boolean} enabled - Enable traversal
|
|
60
|
+
* @property {string} value - Value to be used for traversal
|
|
61
|
+
* @property {Array<*>} items - An array containing the data set to traverse
|
|
62
|
+
* @property {Array<integer} [indices] - Indices of the items to be traversed
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @typedef PositionTracker
|
|
67
|
+
* @property {integer} index
|
|
68
|
+
* @property {integer} previousIndex
|
|
69
|
+
*/
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @typedef EventHandlers
|
|
73
|
+
* @property {function} [keydown]
|
|
74
|
+
* @property {function} [keyup]
|
|
75
|
+
* @property {function} [click]
|
|
76
|
+
* @property {function} [touchstart]
|
|
77
|
+
* @property {function} [touchmove]
|
|
78
|
+
* @property {function} [touchend]
|
|
79
|
+
* @property {function} [touchcancel]
|
|
80
|
+
* @property {function} [mousedown]
|
|
81
|
+
* @property {function} [mouseup]
|
|
82
|
+
* @property {function} [mousemove]
|
|
83
|
+
*/
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @typedef {Object} ActionHandlers
|
|
87
|
+
* @property {Function} [next]
|
|
88
|
+
* @property {Function} [previous]
|
|
89
|
+
* @property {Function} [select]
|
|
90
|
+
* @property {Function} [escape]
|
|
91
|
+
* @property {Function} [collapse]
|
|
92
|
+
* @property {Function} [expand]
|
|
93
|
+
*/
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* @typedef {Object} NavigationOptions
|
|
97
|
+
* @property {Boolean} [horizontal]
|
|
98
|
+
* @property {Boolean} [nested]
|
|
99
|
+
* @property {Boolean} [enabled]
|
|
100
|
+
*/
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @typedef {Object} KeyboardActions
|
|
104
|
+
* @property {Function} [ArrowDown]
|
|
105
|
+
* @property {Function} [ArrowUp]
|
|
106
|
+
* @property {Function} [ArrowRight]
|
|
107
|
+
* @property {Function} [ArrowLeft]
|
|
108
|
+
* @property {Function} [Enter]
|
|
109
|
+
* @property {Function} [Escape]
|
|
110
|
+
* @property {Function} [" "]
|
|
111
|
+
*/
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function handleAction(actions, event) {
|
|
2
|
+
if (event.key in actions) {
|
|
3
|
+
event.preventDefault()
|
|
4
|
+
event.stopPropagation()
|
|
5
|
+
actions[event.key]()
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getKeyboardActions(node, options, handlers) {
|
|
10
|
+
const movement = options.horizontal
|
|
11
|
+
? { ArrowLeft: handlers.previous, ArrowRight: handlers.next }
|
|
12
|
+
: { ArrowUp: handlers.previous, ArrowDown: handlers.next }
|
|
13
|
+
const change = options.nested
|
|
14
|
+
? options.horizontal
|
|
15
|
+
? { ArrowUp: handlers.collapse, ArrowDown: handlers.expand }
|
|
16
|
+
: { ArrowLeft: handlers.collapse, ArrowRight: handlers.expand }
|
|
17
|
+
: {}
|
|
18
|
+
return {
|
|
19
|
+
Enter: handlers.select,
|
|
20
|
+
' ': handlers.select,
|
|
21
|
+
...movement,
|
|
22
|
+
...change
|
|
23
|
+
}
|
|
24
|
+
}
|