@rokkit/actions 1.0.0-next.39 → 1.0.0-next.43
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 +25 -1
- package/src/navigator.js +8 -27
- package/src/swipeable.js +47 -26
- package/src/traversable.js +69 -58
- package/src/types.js +7 -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.43",
|
|
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.43"
|
|
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,4 +1,5 @@
|
|
|
1
1
|
import { compact } from '@rokkit/core'
|
|
2
|
+
import { hasChildren, isExpanded } from '@rokkit/core'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Emits a custom event with the given data.
|
|
@@ -64,7 +65,6 @@ export function getClosestAncestorWithAttribute(element, attribute) {
|
|
|
64
65
|
*/
|
|
65
66
|
export function setupListeners(element, listeners, options) {
|
|
66
67
|
const { enabled } = { enabled: true, ...options }
|
|
67
|
-
|
|
68
68
|
if (enabled) {
|
|
69
69
|
Object.entries(listeners).forEach(([event, listener]) =>
|
|
70
70
|
element.addEventListener(event, listener)
|
|
@@ -87,3 +87,27 @@ export function removeListeners(element, listeners) {
|
|
|
87
87
|
})
|
|
88
88
|
}
|
|
89
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/swipeable.js
CHANGED
|
@@ -1,12 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A svelte action function that captures swipe actions and emits event for corresponding movements.
|
|
3
|
-
*
|
|
4
|
-
* @param {HTMLElement} node
|
|
5
|
-
* @param {import(./types).SwipeableOptions} options
|
|
6
|
-
* @returns {import('./types').SvelteActionReturn}
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
1
|
import { removeListeners, setupListeners } from './lib'
|
|
2
|
+
|
|
10
3
|
const defaultOptions = {
|
|
11
4
|
horizontal: true,
|
|
12
5
|
vertical: false,
|
|
@@ -14,6 +7,14 @@ const defaultOptions = {
|
|
|
14
7
|
enabled: true,
|
|
15
8
|
minSpeed: 300
|
|
16
9
|
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A svelte action function that captures swipe actions and emits event for corresponding movements.
|
|
13
|
+
*
|
|
14
|
+
* @param {HTMLElement} node
|
|
15
|
+
* @param {import(./types).SwipeableOptions} options
|
|
16
|
+
* @returns {import('./types').SvelteActionReturn}
|
|
17
|
+
*/
|
|
17
18
|
export function swipeable(node, options = defaultOptions) {
|
|
18
19
|
let track = {}
|
|
19
20
|
let listeners = {}
|
|
@@ -38,6 +39,13 @@ export function swipeable(node, options = defaultOptions) {
|
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
41
|
|
|
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
|
+
*/
|
|
41
49
|
function getListeners(node, options, track) {
|
|
42
50
|
if (!options.enabled) return {}
|
|
43
51
|
|
|
@@ -50,6 +58,12 @@ function getListeners(node, options, track) {
|
|
|
50
58
|
return listeners
|
|
51
59
|
}
|
|
52
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Handles the touch start event.
|
|
63
|
+
*
|
|
64
|
+
* @param {Event} event
|
|
65
|
+
* @param {import(./types).TouchTracker} track
|
|
66
|
+
*/
|
|
53
67
|
function touchStart(event, track) {
|
|
54
68
|
const touch = event.touches ? event.touches[0] : event
|
|
55
69
|
track.startX = touch.clientX
|
|
@@ -57,31 +71,38 @@ function touchStart(event, track) {
|
|
|
57
71
|
track.startTime = new Date().getTime()
|
|
58
72
|
}
|
|
59
73
|
|
|
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
|
+
*/
|
|
60
82
|
function touchEnd(event, node, options, track) {
|
|
61
|
-
const { horizontal, vertical, threshold, minSpeed } = options
|
|
62
83
|
const touch = event.changedTouches ? event.changedTouches[0] : event
|
|
63
84
|
const distX = touch.clientX - track.startX
|
|
64
85
|
const distY = touch.clientY - track.startY
|
|
65
86
|
const duration = (new Date().getTime() - track.startTime) / 1000
|
|
66
87
|
const speed = Math.max(Math.abs(distX), Math.abs(distY)) / duration
|
|
67
88
|
|
|
68
|
-
if (
|
|
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'))
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
89
|
+
if (speed <= options.minSpeed) return
|
|
77
90
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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'
|
|
86
107
|
}
|
|
87
108
|
}
|
package/src/traversable.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { mappedList, isNested } from '@rokkit/core'
|
|
2
|
+
import { pick } from 'ramda'
|
|
1
3
|
import {
|
|
2
4
|
getClosestAncestorWithAttribute,
|
|
3
5
|
mapKeyboardEventsToActions,
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
emit,
|
|
7
|
+
handleItemClick,
|
|
8
|
+
EventManager
|
|
7
9
|
} from './lib'
|
|
8
10
|
|
|
9
11
|
const defaultOptions = {
|
|
@@ -19,72 +21,81 @@ const defaultOptions = {
|
|
|
19
21
|
* @param {import('./types').TraversableOptions} data
|
|
20
22
|
* @returns
|
|
21
23
|
*/
|
|
22
|
-
export function traversable(element,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
let handlers
|
|
26
|
-
let actions
|
|
27
|
-
let listeners
|
|
24
|
+
export function traversable(element, options) {
|
|
25
|
+
const content = mappedList(options.items, options.fields)
|
|
26
|
+
const manager = EventManager(element)
|
|
28
27
|
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
let current = { position: [], item: null }
|
|
29
|
+
let handlers = {}
|
|
31
30
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
const moveCursor = (direction) => {
|
|
32
|
+
const result = content[direction](current.position)
|
|
33
|
+
if (result) {
|
|
34
|
+
current = result
|
|
35
|
+
|
|
36
|
+
checkAndEmit('move')
|
|
37
|
+
}
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
|
|
40
|
+
const checkAndEmit = (event) => {
|
|
41
|
+
if (current && current.item) {
|
|
42
|
+
emit(element, event, pick(['item', 'position'], current))
|
|
43
|
+
}
|
|
44
|
+
}
|
|
40
45
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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')
|
|
44
53
|
}
|
|
45
|
-
}
|
|
46
54
|
|
|
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 {
|
|
55
|
+
const listeners = {
|
|
56
56
|
keydown: (event) => {
|
|
57
|
-
|
|
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
|
-
}
|
|
57
|
+
if (event.key in handlers) handlers[event.key](event)
|
|
68
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
|
+
// }
|
|
69
70
|
}
|
|
70
|
-
}
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
72
|
+
const update = (data) => {
|
|
73
|
+
options = { ...defaultOptions, ...options, ...data }
|
|
74
|
+
options.nested = isNested(options.items, options.fields)
|
|
75
|
+
content.update(options.items, options.fields)
|
|
76
|
+
handlers = mapKeyboardEventsToActions(actions, options)
|
|
77
|
+
manager.update(options.enabled, listeners)
|
|
78
|
+
// current = handleValueChange(element, data, content, current)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
update(options)
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
update,
|
|
85
|
+
destroy: () => manager.destroy()
|
|
86
86
|
}
|
|
87
|
-
return actions
|
|
88
87
|
}
|
|
89
88
|
|
|
90
|
-
//
|
|
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
|
+
// }
|
|
96
|
+
|
|
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
|
@@ -109,3 +109,10 @@
|
|
|
109
109
|
* @property {Function} [Escape]
|
|
110
110
|
* @property {Function} [" "]
|
|
111
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
|
+
*/
|