@rokkit/actions 1.0.0-next.38 → 1.0.0-next.42
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 +6 -4
- package/src/hierarchy.js +19 -10
- package/src/lib/event-manager.js +32 -0
- package/src/lib/index.js +1 -0
- package/src/lib/internal.js +78 -28
- package/src/navigator.js +8 -27
- package/src/pannable.js +21 -18
- package/src/swipeable.js +88 -72
- package/src/traversable.js +75 -51
- package/src/types.js +65 -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.42",
|
|
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",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"devDependencies": {
|
|
16
16
|
"@sveltejs/vite-plugin-svelte": "^2.4.3",
|
|
17
17
|
"@testing-library/svelte": "^4.0.3",
|
|
18
|
+
"@types/ramda": "^0.29.3",
|
|
18
19
|
"@vitest/coverage-v8": "^0.33.0",
|
|
19
20
|
"@vitest/ui": "~0.33.0",
|
|
20
21
|
"jsdom": "^22.1.0",
|
|
@@ -23,7 +24,7 @@
|
|
|
23
24
|
"validators": "latest",
|
|
24
25
|
"vite": "^4.4.7",
|
|
25
26
|
"vitest": "~0.33.0",
|
|
26
|
-
"shared-config": "1.0.0-next.
|
|
27
|
+
"shared-config": "1.0.0-next.42"
|
|
27
28
|
},
|
|
28
29
|
"files": [
|
|
29
30
|
"src/**/*.js",
|
|
@@ -41,7 +42,8 @@
|
|
|
41
42
|
},
|
|
42
43
|
"dependencies": {
|
|
43
44
|
"@rokkit/core": "latest",
|
|
44
|
-
"@rokkit/stores": "latest"
|
|
45
|
+
"@rokkit/stores": "latest",
|
|
46
|
+
"ramda": "^0.29.0"
|
|
45
47
|
},
|
|
46
48
|
"scripts": {
|
|
47
49
|
"format": "prettier --write .",
|
|
@@ -52,6 +54,6 @@
|
|
|
52
54
|
"test": "vitest",
|
|
53
55
|
"coverage": "vitest run --coverage",
|
|
54
56
|
"latest": "pnpm upgrade --latest && pnpm test:ci",
|
|
55
|
-
"release": "
|
|
57
|
+
"release": "pnpm publish --access public"
|
|
56
58
|
}
|
|
57
59
|
}
|
package/src/hierarchy.js
CHANGED
|
@@ -46,20 +46,29 @@ export function moveNext(path, items, fields) {
|
|
|
46
46
|
} else if (current.index < current.items.length - 1) {
|
|
47
47
|
current.index++
|
|
48
48
|
} else {
|
|
49
|
-
|
|
50
|
-
while (level >= 0) {
|
|
51
|
-
const parent = path[level]
|
|
52
|
-
if (parent.index < parent.items.length - 1) {
|
|
53
|
-
parent.index++
|
|
54
|
-
path = path.slice(0, level + 1)
|
|
55
|
-
break
|
|
56
|
-
}
|
|
57
|
-
level--
|
|
58
|
-
}
|
|
49
|
+
path = navigateToNextLevel(path)
|
|
59
50
|
}
|
|
60
51
|
return path
|
|
61
52
|
}
|
|
62
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Navigate to the next level
|
|
56
|
+
* @param {Array<import('./types').PathFragment>} path
|
|
57
|
+
* @returns {Array<import('./types').PathFragment>}
|
|
58
|
+
*/
|
|
59
|
+
function navigateToNextLevel(path) {
|
|
60
|
+
let level = path.length - 2
|
|
61
|
+
while (level >= 0) {
|
|
62
|
+
const parent = path[level]
|
|
63
|
+
if (parent.index < parent.items.length - 1) {
|
|
64
|
+
parent.index++
|
|
65
|
+
path = path.slice(0, level + 1)
|
|
66
|
+
break
|
|
67
|
+
}
|
|
68
|
+
level--
|
|
69
|
+
}
|
|
70
|
+
return path
|
|
71
|
+
}
|
|
63
72
|
/**
|
|
64
73
|
* Navigate to the previous item
|
|
65
74
|
*
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EventManager class to manage event listeners on an element.
|
|
3
|
+
*/
|
|
4
|
+
export function EventManager(element, handlers = {}) {
|
|
5
|
+
let listening = false
|
|
6
|
+
|
|
7
|
+
function activate() {
|
|
8
|
+
if (!listening) {
|
|
9
|
+
for (const event in handlers) {
|
|
10
|
+
element.addEventListener(event, handlers[event])
|
|
11
|
+
}
|
|
12
|
+
listening = true
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function destroy() {
|
|
16
|
+
if (listening) {
|
|
17
|
+
for (const event in handlers) {
|
|
18
|
+
element.removeEventListener(event, handlers[event])
|
|
19
|
+
}
|
|
20
|
+
listening = false
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function update(enabled, newHandlers = handlers) {
|
|
24
|
+
if (listening !== enabled || handlers !== newHandlers) {
|
|
25
|
+
destroy()
|
|
26
|
+
handlers = newHandlers
|
|
27
|
+
if (enabled) activate()
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return { activate, destroy, update }
|
|
32
|
+
}
|
package/src/lib/index.js
CHANGED
package/src/lib/internal.js
CHANGED
|
@@ -1,39 +1,24 @@
|
|
|
1
1
|
import { compact } from '@rokkit/core'
|
|
2
|
+
import { hasChildren, isExpanded } from '@rokkit/core'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* @
|
|
7
|
-
* @
|
|
8
|
-
* @
|
|
9
|
-
* @
|
|
10
|
-
* @property {Function} [expand]
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* @typedef {Object} NavigationOptions
|
|
15
|
-
* @property {Boolean} [horizontal]
|
|
16
|
-
* @property {Boolean} [nested]
|
|
17
|
-
* @property {Boolean} [enabled]
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* @typedef {Object} KeyboardActions
|
|
22
|
-
* @property {Function} [ArrowDown]
|
|
23
|
-
* @property {Function} [ArrowUp]
|
|
24
|
-
* @property {Function} [ArrowRight]
|
|
25
|
-
* @property {Function} [ArrowLeft]
|
|
26
|
-
* @property {Function} [Enter]
|
|
27
|
-
* @property {Function} [Escape]
|
|
28
|
-
* @property {Function} [" "]
|
|
5
|
+
* Emits a custom event with the given data.
|
|
6
|
+
*
|
|
7
|
+
* @param {HTMLElement} element
|
|
8
|
+
* @param {string} event
|
|
9
|
+
* @param {*} data
|
|
10
|
+
* @returns {void}
|
|
29
11
|
*/
|
|
12
|
+
export function emit(element, event, data) {
|
|
13
|
+
element.dispatchEvent(new CustomEvent(event, { detail: data }))
|
|
14
|
+
}
|
|
30
15
|
|
|
31
16
|
/**
|
|
32
17
|
* Maps keyboard events to actions based on the given handlers and options.
|
|
33
18
|
*
|
|
34
|
-
* @param {ActionHandlers} handlers
|
|
35
|
-
* @param {NavigationOptions} options
|
|
36
|
-
* @returns {KeyboardActions}
|
|
19
|
+
* @param {import('../types').ActionHandlers} handlers
|
|
20
|
+
* @param {import('../types').NavigationOptions} options
|
|
21
|
+
* @returns {import('../types').KeyboardActions}
|
|
37
22
|
*/
|
|
38
23
|
export function mapKeyboardEventsToActions(handlers, options) {
|
|
39
24
|
const { next, previous, select, escape } = handlers
|
|
@@ -56,8 +41,73 @@ export function mapKeyboardEventsToActions(handlers, options) {
|
|
|
56
41
|
})
|
|
57
42
|
}
|
|
58
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Finds the closest ancestor of the given element that has the given attribute.
|
|
46
|
+
*
|
|
47
|
+
* @param {HTMLElement} element
|
|
48
|
+
* @param {string} attribute
|
|
49
|
+
* @returns {HTMLElement|null}
|
|
50
|
+
*/
|
|
59
51
|
export function getClosestAncestorWithAttribute(element, attribute) {
|
|
60
52
|
if (!element) return null
|
|
61
53
|
if (element.getAttribute(attribute)) return element
|
|
62
54
|
return getClosestAncestorWithAttribute(element.parentElement, attribute)
|
|
63
55
|
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Sets up event handlers based on the given options.
|
|
59
|
+
* Returns whether or not the event handlers are listening.
|
|
60
|
+
*
|
|
61
|
+
* @param {HTMLElement} element
|
|
62
|
+
* @param {import('../types').EventHandlers} listeners
|
|
63
|
+
* @param {import('../types').TraversableOptions} options
|
|
64
|
+
* @returns {void}
|
|
65
|
+
*/
|
|
66
|
+
export function setupListeners(element, listeners, options) {
|
|
67
|
+
const { enabled } = { enabled: true, ...options }
|
|
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
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Handles the click event.
|
|
93
|
+
* @param {HTMLElement} element - The root element.
|
|
94
|
+
* @param {CurrentItem} current - A reference to the current Item
|
|
95
|
+
* @returns {CurrentItem} The updated current item.
|
|
96
|
+
*/
|
|
97
|
+
export function handleItemClick(element, current) {
|
|
98
|
+
const { item, fields, position } = current
|
|
99
|
+
const detail = { item, position }
|
|
100
|
+
|
|
101
|
+
if (hasChildren(item, fields)) {
|
|
102
|
+
if (isExpanded(item, fields)) {
|
|
103
|
+
item[fields.isOpen] = false
|
|
104
|
+
emit(element, 'collapse', detail)
|
|
105
|
+
} else {
|
|
106
|
+
item[fields.isOpen] = true
|
|
107
|
+
emit(element, 'expand', detail)
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
emit(element, 'select', detail)
|
|
111
|
+
}
|
|
112
|
+
return current
|
|
113
|
+
}
|
package/src/navigator.js
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
indicesFromPath,
|
|
8
8
|
getCurrentNode
|
|
9
9
|
} from './hierarchy'
|
|
10
|
-
|
|
10
|
+
import { mapKeyboardEventsToActions } from './lib'
|
|
11
11
|
/**
|
|
12
12
|
* Keyboard navigation for Lists and NestedLists. The data is either nested or not and is not
|
|
13
13
|
* expected to switch from nested to simple list or vice-versa.
|
|
@@ -83,11 +83,15 @@ export function navigator(element, options) {
|
|
|
83
83
|
update(options)
|
|
84
84
|
|
|
85
85
|
const nested = isNested(items, fields)
|
|
86
|
-
const actions = mapKeyboardEventsToActions(
|
|
86
|
+
const actions = mapKeyboardEventsToActions(handlers, {
|
|
87
|
+
horizontal: !vertical,
|
|
88
|
+
nested
|
|
89
|
+
})
|
|
87
90
|
|
|
88
91
|
const handleKeyDown = (event) => handleAction(actions, event)
|
|
89
92
|
|
|
90
93
|
const handleClick = (event) => {
|
|
94
|
+
event.stopPropagation()
|
|
91
95
|
let target = findParentWithDataPath(event.target, element)
|
|
92
96
|
let indices = !target
|
|
93
97
|
? []
|
|
@@ -106,7 +110,8 @@ export function navigator(element, options) {
|
|
|
106
110
|
? 'expand'
|
|
107
111
|
: 'collapse'
|
|
108
112
|
emit(event, element, indices, currentNode)
|
|
109
|
-
} else if (currentNode
|
|
113
|
+
} else if (currentNode !== null)
|
|
114
|
+
emit('select', element, indices, currentNode)
|
|
110
115
|
}
|
|
111
116
|
}
|
|
112
117
|
|
|
@@ -174,27 +179,3 @@ function emit(event, element, indices, node) {
|
|
|
174
179
|
})
|
|
175
180
|
)
|
|
176
181
|
}
|
|
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
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
import { removeListeners, setupListeners } from './lib'
|
|
2
|
+
|
|
3
|
+
const defaultOptions = {
|
|
4
|
+
horizontal: true,
|
|
5
|
+
vertical: false,
|
|
6
|
+
threshold: 100,
|
|
7
|
+
enabled: true,
|
|
8
|
+
minSpeed: 300
|
|
9
|
+
}
|
|
10
|
+
|
|
1
11
|
/**
|
|
2
12
|
* A svelte action function that captures swipe actions and emits event for corresponding movements.
|
|
3
13
|
*
|
|
@@ -5,88 +15,94 @@
|
|
|
5
15
|
* @param {import(./types).SwipeableOptions} options
|
|
6
16
|
* @returns {import('./types').SvelteActionReturn}
|
|
7
17
|
*/
|
|
18
|
+
export function swipeable(node, options = defaultOptions) {
|
|
19
|
+
let track = {}
|
|
20
|
+
let listeners = {}
|
|
8
21
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
vertical = false,
|
|
14
|
-
threshold = 100,
|
|
15
|
-
enabled = true,
|
|
16
|
-
minSpeed = 300
|
|
17
|
-
} = {}
|
|
18
|
-
) {
|
|
19
|
-
let listening = false
|
|
20
|
-
let startX
|
|
21
|
-
let startY
|
|
22
|
-
let startTime
|
|
23
|
-
|
|
24
|
-
function touchStart(event) {
|
|
25
|
-
const touch = event.touches ? event.touches[0] : event
|
|
26
|
-
startX = touch.clientX
|
|
27
|
-
startY = touch.clientY
|
|
28
|
-
startTime = new Date().getTime()
|
|
22
|
+
const updateListeners = (options) => {
|
|
23
|
+
removeListeners(node, listeners)
|
|
24
|
+
listeners = getListeners(node, options, track)
|
|
25
|
+
setupListeners(node, listeners, options)
|
|
29
26
|
}
|
|
30
27
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const distX = touch.clientX - startX
|
|
34
|
-
const distY = touch.clientY - startY
|
|
35
|
-
const duration = (new Date().getTime() - startTime) / 1000
|
|
36
|
-
const speed = Math.max(Math.abs(distX), Math.abs(distY)) / duration
|
|
28
|
+
options = { ...defaultOptions, ...options }
|
|
29
|
+
updateListeners(options)
|
|
37
30
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (vertical && speed > minSpeed) {
|
|
49
|
-
if (Math.abs(distY) > Math.abs(distX) && Math.abs(distY) >= threshold) {
|
|
50
|
-
if (distY > 0) {
|
|
51
|
-
node.dispatchEvent(new CustomEvent('swipeDown'))
|
|
52
|
-
} else {
|
|
53
|
-
node.dispatchEvent(new CustomEvent('swipeUp'))
|
|
54
|
-
}
|
|
55
|
-
}
|
|
31
|
+
return {
|
|
32
|
+
update: (data) => {
|
|
33
|
+
options = { ...options, ...data }
|
|
34
|
+
updateListeners(options)
|
|
35
|
+
},
|
|
36
|
+
destroy() {
|
|
37
|
+
removeListeners(node, listeners)
|
|
56
38
|
}
|
|
57
39
|
}
|
|
40
|
+
}
|
|
58
41
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Returns the listeners for the swipeable action.
|
|
44
|
+
* @param {HTMLElement} node - The node where the event is dispatched.
|
|
45
|
+
* @param {import(./types).SwipeableOptions} options - The options for the swipe.
|
|
46
|
+
* @param {import(./types).TouchTracker} track - The tracking object.
|
|
47
|
+
* @returns {import(./types).Listeners}
|
|
48
|
+
*/
|
|
49
|
+
function getListeners(node, options, track) {
|
|
50
|
+
if (!options.enabled) return {}
|
|
51
|
+
|
|
52
|
+
let listeners = {
|
|
53
|
+
touchend: (e) => touchEnd(e, node, options, track),
|
|
54
|
+
touchstart: (e) => touchStart(e, track),
|
|
55
|
+
mousedown: (e) => touchStart(e, track),
|
|
56
|
+
mouseup: (e) => touchEnd(e, node, options, track)
|
|
74
57
|
}
|
|
58
|
+
return listeners
|
|
59
|
+
}
|
|
75
60
|
|
|
76
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Handles the touch start event.
|
|
63
|
+
*
|
|
64
|
+
* @param {Event} event
|
|
65
|
+
* @param {import(./types).TouchTracker} track
|
|
66
|
+
*/
|
|
67
|
+
function touchStart(event, track) {
|
|
68
|
+
const touch = event.touches ? event.touches[0] : event
|
|
69
|
+
track.startX = touch.clientX
|
|
70
|
+
track.startY = touch.clientY
|
|
71
|
+
track.startTime = new Date().getTime()
|
|
72
|
+
}
|
|
77
73
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
74
|
+
/**
|
|
75
|
+
* Handles the touch end event.
|
|
76
|
+
*
|
|
77
|
+
* @param {Event} event - The touch event.
|
|
78
|
+
* @param {HTMLElement} node - The node where the event is dispatched.
|
|
79
|
+
* @param {import(./types).SwipeableOptions} options options - The options for the swipe.
|
|
80
|
+
* @param {import(./types).TouchTracker} track - The tracking object.
|
|
81
|
+
*/
|
|
82
|
+
function touchEnd(event, node, options, track) {
|
|
83
|
+
const touch = event.changedTouches ? event.changedTouches[0] : event
|
|
84
|
+
const distX = touch.clientX - track.startX
|
|
85
|
+
const distY = touch.clientY - track.startY
|
|
86
|
+
const duration = (new Date().getTime() - track.startTime) / 1000
|
|
87
|
+
const speed = Math.max(Math.abs(distX), Math.abs(distY)) / duration
|
|
85
88
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
if (speed <= options.minSpeed) return
|
|
90
|
+
|
|
91
|
+
const isHorizontalSwipe =
|
|
92
|
+
options.horizontal && Math.abs(distX) >= options.threshold
|
|
93
|
+
const isVerticalSwipe =
|
|
94
|
+
options.vertical && Math.abs(distY) >= options.threshold
|
|
95
|
+
|
|
96
|
+
if (!isHorizontalSwipe && !isVerticalSwipe) return
|
|
97
|
+
|
|
98
|
+
const swipeDirection = getSwipeDirection(distX, distY)
|
|
99
|
+
node.dispatchEvent(new CustomEvent(`swipe${swipeDirection}`))
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getSwipeDirection(distX, distY) {
|
|
103
|
+
if (Math.abs(distX) > Math.abs(distY)) {
|
|
104
|
+
return distX > 0 ? 'Right' : 'Left'
|
|
105
|
+
} else {
|
|
106
|
+
return distY > 0 ? 'Down' : 'Up'
|
|
91
107
|
}
|
|
92
108
|
}
|
package/src/traversable.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
import { mappedList, isNested } from '@rokkit/core'
|
|
2
|
+
import { pick } from 'ramda'
|
|
1
3
|
import {
|
|
2
4
|
getClosestAncestorWithAttribute,
|
|
3
|
-
mapKeyboardEventsToActions
|
|
5
|
+
mapKeyboardEventsToActions,
|
|
6
|
+
emit,
|
|
7
|
+
handleItemClick,
|
|
8
|
+
EventManager
|
|
4
9
|
} from './lib'
|
|
5
10
|
|
|
6
11
|
const defaultOptions = {
|
|
@@ -8,70 +13,89 @@ const defaultOptions = {
|
|
|
8
13
|
nested: false,
|
|
9
14
|
enabled: true
|
|
10
15
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* An action that can be used to traverse a nested list of items using keyboard and mouse.
|
|
19
|
+
*
|
|
20
|
+
* @param {HTMLElement} element
|
|
21
|
+
* @param {import('./types').TraversableOptions} data
|
|
22
|
+
* @returns
|
|
23
|
+
*/
|
|
24
|
+
export function traversable(element, options) {
|
|
25
|
+
const content = mappedList(options.items, options.fields)
|
|
26
|
+
const manager = EventManager(element)
|
|
27
|
+
|
|
28
|
+
let current = { position: [], item: null }
|
|
15
29
|
let handlers = {}
|
|
16
30
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
escape: () => emit(element, 'escape', tracker),
|
|
22
|
-
collapse: () => emit(element, 'collapse', tracker),
|
|
23
|
-
expand: () => emit(element, 'expand', tracker)
|
|
24
|
-
}
|
|
31
|
+
const moveCursor = (direction) => {
|
|
32
|
+
const result = content[direction](current.position)
|
|
33
|
+
if (result) {
|
|
34
|
+
current = result
|
|
25
35
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (action) action(event)
|
|
30
|
-
},
|
|
31
|
-
click: (event) => {
|
|
32
|
-
const target = getClosestAncestorWithAttribute(event.target, 'data-index')
|
|
36
|
+
checkAndEmit('move')
|
|
37
|
+
}
|
|
38
|
+
}
|
|
33
39
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
actions.select()
|
|
38
|
-
}
|
|
40
|
+
const checkAndEmit = (event) => {
|
|
41
|
+
if (current && current.item) {
|
|
42
|
+
emit(element, event, pick(['item', 'position'], current))
|
|
39
43
|
}
|
|
40
44
|
}
|
|
41
45
|
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
const actions = {
|
|
47
|
+
next: () => moveCursor('next'),
|
|
48
|
+
previous: () => moveCursor('previous'),
|
|
49
|
+
select: () => checkAndEmit('select'),
|
|
50
|
+
escape: () => checkAndEmit('escape'),
|
|
51
|
+
collapse: () => checkAndEmit('collapse'),
|
|
52
|
+
expand: () => checkAndEmit('expand')
|
|
53
|
+
}
|
|
45
54
|
|
|
46
|
-
|
|
55
|
+
const listeners = {
|
|
56
|
+
keydown: (event) => {
|
|
57
|
+
if (event.key in handlers) handlers[event.key](event)
|
|
58
|
+
}
|
|
59
|
+
// click: (event) => {
|
|
60
|
+
// const target = getClosestAncestorWithAttribute(event.target, 'data-path')
|
|
61
|
+
// if (target) {
|
|
62
|
+
// const position = target
|
|
63
|
+
// .getAttribute('data-path')
|
|
64
|
+
// .split(',')
|
|
65
|
+
// .map((i) => +i)
|
|
66
|
+
// current = content.findByPosition(position)
|
|
67
|
+
// current = handleItemClick(element, current)
|
|
68
|
+
// }
|
|
69
|
+
// }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const update = (data) => {
|
|
73
|
+
options = { ...defaultOptions, ...options, ...data }
|
|
74
|
+
options.nested = isNested(options.items, options.fields)
|
|
75
|
+
content.update(options.items, options.fields)
|
|
47
76
|
handlers = mapKeyboardEventsToActions(actions, options)
|
|
48
|
-
|
|
77
|
+
manager.update(options.enabled, listeners)
|
|
78
|
+
// current = handleValueChange(element, data, content, current)
|
|
49
79
|
}
|
|
50
80
|
|
|
51
|
-
|
|
81
|
+
update(options)
|
|
52
82
|
|
|
53
83
|
return {
|
|
54
|
-
update
|
|
55
|
-
destroy: () =>
|
|
84
|
+
update,
|
|
85
|
+
destroy: () => manager.destroy()
|
|
56
86
|
}
|
|
57
87
|
}
|
|
58
88
|
|
|
59
|
-
|
|
60
|
-
|
|
89
|
+
// const handleValueChange = (element, data, content, current) => {
|
|
90
|
+
// if (data.value !== null && data.value !== current?.value) {
|
|
91
|
+
// current = content.findByValue(data.value)
|
|
92
|
+
// if (current) scrollIntoView(element, current.position)
|
|
93
|
+
// }
|
|
94
|
+
// return current
|
|
95
|
+
// }
|
|
61
96
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
Object.entries(handlers).forEach(([event, handler]) =>
|
|
68
|
-
element.removeEventListener(event, handler)
|
|
69
|
-
)
|
|
70
|
-
}
|
|
71
|
-
return enabled
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function emit(element, event, tracker) {
|
|
75
|
-
element.dispatchEvent(new CustomEvent(event, { detail: tracker }))
|
|
76
|
-
}
|
|
77
|
-
// function handleValueChange(element, options) {}
|
|
97
|
+
// function scrollIntoView(element, position) {
|
|
98
|
+
// if (!Array.isArray(position) || position.length == 0) return
|
|
99
|
+
// const node = element.querySelector(`[data-index="${position.join(',')}"]`)
|
|
100
|
+
// if (node) node.scrollIntoView()
|
|
101
|
+
// }
|
package/src/types.js
CHANGED
|
@@ -51,3 +51,68 @@
|
|
|
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
|
+
*/
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @typedef {Object} TouchTracker
|
|
115
|
+
* @property {number} startX - The start X position of the touch.
|
|
116
|
+
* @property {number} startY - The start Y position of the touch.
|
|
117
|
+
* @property {number} startTime - The start time of the touch.
|
|
118
|
+
*/
|