@rokkit/actions 1.0.0-next.36 → 1.0.0-next.38
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 +5 -5
- package/src/dismissable.js +10 -1
- package/src/fillable.js +10 -16
- package/src/hierarchy.js +25 -58
- package/src/index.js +1 -0
- package/src/lib/index.js +1 -0
- package/src/lib/internal.js +63 -0
- package/src/navigable.js +37 -32
- package/src/navigator.js +96 -103
- package/src/pannable.js +2 -3
- package/src/swipeable.js +36 -9
- package/src/themeable.js +9 -1
- package/src/traversable.js +77 -0
- package/src/types.js +53 -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.38",
|
|
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",
|
|
@@ -13,17 +13,17 @@
|
|
|
13
13
|
"access": "public"
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
16
|
-
"@sveltejs/vite-plugin-svelte": "^2.4.
|
|
16
|
+
"@sveltejs/vite-plugin-svelte": "^2.4.3",
|
|
17
17
|
"@testing-library/svelte": "^4.0.3",
|
|
18
18
|
"@vitest/coverage-v8": "^0.33.0",
|
|
19
19
|
"@vitest/ui": "~0.33.0",
|
|
20
20
|
"jsdom": "^22.1.0",
|
|
21
|
-
"svelte": "^4.
|
|
21
|
+
"svelte": "^4.1.1",
|
|
22
22
|
"typescript": "^5.1.6",
|
|
23
23
|
"validators": "latest",
|
|
24
|
-
"vite": "^4.4.
|
|
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.38"
|
|
27
27
|
},
|
|
28
28
|
"files": [
|
|
29
29
|
"src/**/*.js",
|
package/src/dismissable.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
const KEYCODE_ESC = 27
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* A svelte action function that captures clicks outside the element or escape keypress
|
|
5
|
+
* emits a `dismiss` event. This is useful for closing a modal or dropdown.
|
|
6
|
+
*
|
|
7
|
+
* @param {HTMLElement} node
|
|
8
|
+
* @returns {import('./types').SvelteActionReturn}
|
|
9
|
+
*/
|
|
3
10
|
export function dismissable(node) {
|
|
4
11
|
const handleClick = (event) => {
|
|
5
12
|
if (node && !node.contains(event.target) && !event.defaultPrevented) {
|
|
@@ -7,7 +14,9 @@ export function dismissable(node) {
|
|
|
7
14
|
}
|
|
8
15
|
}
|
|
9
16
|
const keyup = (event) => {
|
|
10
|
-
if (event.keyCode === KEYCODE_ESC) {
|
|
17
|
+
if (event.keyCode === KEYCODE_ESC || event.key === 'Escape') {
|
|
18
|
+
event.stopPropagation()
|
|
19
|
+
|
|
11
20
|
node.dispatchEvent(new CustomEvent('dismiss', node))
|
|
12
21
|
}
|
|
13
22
|
}
|
package/src/fillable.js
CHANGED
|
@@ -1,14 +1,8 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @typedef FillOptions
|
|
3
|
-
* @property {Array<string>} options available options to fill
|
|
4
|
-
* @property {integer} current index of option to be filled
|
|
5
|
-
* @property {boolean} check validate filled values
|
|
6
|
-
*/
|
|
7
1
|
/**
|
|
8
2
|
* Action for filling a <del>?</del> element in html block.
|
|
9
3
|
*
|
|
10
|
-
* @param {
|
|
11
|
-
* @param {FillOptions} options
|
|
4
|
+
* @param {HTMLElement} node
|
|
5
|
+
* @param {import('./types').FillOptions} options
|
|
12
6
|
* @returns
|
|
13
7
|
*/
|
|
14
8
|
export function fillable(node, { options, current, check }) {
|
|
@@ -43,8 +37,8 @@ export function fillable(node, { options, current, check }) {
|
|
|
43
37
|
/**
|
|
44
38
|
* Initialize empty fillable element style and add listener for click
|
|
45
39
|
*
|
|
46
|
-
* @param {
|
|
47
|
-
* @param {
|
|
40
|
+
* @param {HTMLCollection} blanks
|
|
41
|
+
* @param {EventListener} click
|
|
48
42
|
*/
|
|
49
43
|
function initialize(blanks, click) {
|
|
50
44
|
Object.keys(blanks).map((ref) => {
|
|
@@ -58,8 +52,8 @@ function initialize(blanks, click) {
|
|
|
58
52
|
/**
|
|
59
53
|
* Fill current blank with provided option
|
|
60
54
|
*
|
|
61
|
-
* @param {
|
|
62
|
-
* @param {
|
|
55
|
+
* @param {HTMLCollection} blanks
|
|
56
|
+
* @param {Array<import('./types.js').FillableData>} options
|
|
63
57
|
* @param {*} current
|
|
64
58
|
*/
|
|
65
59
|
function fill(blanks, options, current) {
|
|
@@ -76,8 +70,8 @@ function fill(blanks, options, current) {
|
|
|
76
70
|
/**
|
|
77
71
|
* Clear all fillable elements
|
|
78
72
|
*
|
|
79
|
-
* @param {
|
|
80
|
-
* @param {
|
|
73
|
+
* @param {EventListener} event
|
|
74
|
+
* @param {HTMLElement} node
|
|
81
75
|
*/
|
|
82
76
|
function clear(event, node) {
|
|
83
77
|
event.target.innerHTML = '?'
|
|
@@ -98,8 +92,8 @@ function clear(event, node) {
|
|
|
98
92
|
/**
|
|
99
93
|
* Validate the filled values
|
|
100
94
|
*
|
|
101
|
-
* @param {
|
|
102
|
-
* @param {
|
|
95
|
+
* @param {HTMLCollection} blanks
|
|
96
|
+
* @param {import('./types').FillOptions} data
|
|
103
97
|
*/
|
|
104
98
|
function validate(blanks, data) {
|
|
105
99
|
Object.keys(blanks).map((ref) => {
|
package/src/hierarchy.js
CHANGED
|
@@ -1,61 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
* A part of the path to node in hierarchy
|
|
3
|
-
*
|
|
4
|
-
* @typedef PathFragment
|
|
5
|
-
* @property {integer} index - Index to item in array
|
|
6
|
-
* @property {Array<*>} items - Array of items
|
|
7
|
-
* @property {import('../constants').FieldMapping} fields - Field mapping for the data
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Check if the current item is a parent
|
|
12
|
-
*
|
|
13
|
-
* @param {*} item
|
|
14
|
-
* @param {import('../constants').FieldMapping} fields
|
|
15
|
-
* @returns {boolean}
|
|
16
|
-
*/
|
|
17
|
-
export function hasChildren(item, fields) {
|
|
18
|
-
return (
|
|
19
|
-
typeof item === 'object' &&
|
|
20
|
-
fields.children in item &&
|
|
21
|
-
Array.isArray(item[fields.children])
|
|
22
|
-
)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Check if the current item is a parent and is expanded
|
|
27
|
-
*
|
|
28
|
-
* @param {*} item
|
|
29
|
-
* @param {import('../constants').FieldMapping} fields
|
|
30
|
-
* @returns {boolean}
|
|
31
|
-
*/
|
|
32
|
-
export function isExpanded(item, fields) {
|
|
33
|
-
if (item == null) return false
|
|
34
|
-
if (!hasChildren(item, fields)) return false
|
|
35
|
-
if (fields.isOpen in item) {
|
|
36
|
-
return item[fields.isOpen]
|
|
37
|
-
}
|
|
38
|
-
return false
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Verify if at least one item has children
|
|
43
|
-
*
|
|
44
|
-
* @param {Array<*>} items
|
|
45
|
-
* @param {import('../constants').FieldMapping} fields
|
|
46
|
-
* @returns {boolean}
|
|
47
|
-
*/
|
|
48
|
-
export function isNested(items, fields) {
|
|
49
|
-
for (let i = 0; i < items.length; i++) {
|
|
50
|
-
if (hasChildren(items[i], fields)) return true
|
|
51
|
-
}
|
|
52
|
-
return false
|
|
53
|
-
}
|
|
1
|
+
import { isExpanded } from '@rokkit/core'
|
|
54
2
|
|
|
55
3
|
/**
|
|
56
4
|
* Navigate to last visible child in the hirarchy starting with the provided path
|
|
57
5
|
*
|
|
58
|
-
* @param {Array<PathFragment>} path - path to a node in the hierarchy
|
|
6
|
+
* @param {Array<import('./types').PathFragment>} path - path to a node in the hierarchy
|
|
59
7
|
* @returns
|
|
60
8
|
*/
|
|
61
9
|
export function navigateToLastVisibleChild(path) {
|
|
@@ -78,9 +26,9 @@ export function navigateToLastVisibleChild(path) {
|
|
|
78
26
|
/**
|
|
79
27
|
* Navigate to the next item
|
|
80
28
|
*
|
|
81
|
-
* @param {Array<PathFragment>} path - path to a node in the hierarchy
|
|
29
|
+
* @param {Array<import('./types').PathFragment>} path - path to a node in the hierarchy
|
|
82
30
|
* @param {Array<*>} items - array of items
|
|
83
|
-
* @param {import('
|
|
31
|
+
* @param {import('@rokkit/core').FieldMapping} fields - field mapping
|
|
84
32
|
* @returns
|
|
85
33
|
*/
|
|
86
34
|
export function moveNext(path, items, fields) {
|
|
@@ -115,7 +63,7 @@ export function moveNext(path, items, fields) {
|
|
|
115
63
|
/**
|
|
116
64
|
* Navigate to the previous item
|
|
117
65
|
*
|
|
118
|
-
* @param {Array<PathFragment>} path - path to a node in the hierarchy
|
|
66
|
+
* @param {Array<import('./types').PathFragment>} path - path to a node in the hierarchy
|
|
119
67
|
* @returns
|
|
120
68
|
*/
|
|
121
69
|
export function movePrevious(path) {
|
|
@@ -139,7 +87,7 @@ export function movePrevious(path) {
|
|
|
139
87
|
*
|
|
140
88
|
* @param {Array<integer>} indices
|
|
141
89
|
* @param {Array<*>} items
|
|
142
|
-
* @param {import('
|
|
90
|
+
* @param {import('@rokkit/core').FieldMapping} fields
|
|
143
91
|
* @returns
|
|
144
92
|
*/
|
|
145
93
|
export function pathFromIndices(indices, items, fields) {
|
|
@@ -160,15 +108,34 @@ export function pathFromIndices(indices, items, fields) {
|
|
|
160
108
|
return path
|
|
161
109
|
}
|
|
162
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Get the indices from the path
|
|
113
|
+
* @param {Array<import('./types').PathFragment>} path
|
|
114
|
+
* @returns {Array<integer>}
|
|
115
|
+
*/
|
|
163
116
|
export function indicesFromPath(path) {
|
|
164
117
|
return path.map(({ index }) => index)
|
|
165
118
|
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Get the current node from the path
|
|
122
|
+
* @param {Array<import('./types').PathFragment>} path
|
|
123
|
+
* @returns {*}
|
|
124
|
+
*/
|
|
166
125
|
export function getCurrentNode(path) {
|
|
167
126
|
if (path.length === 0) return null
|
|
168
127
|
const lastIndex = path.length - 1
|
|
169
128
|
return path[lastIndex].items[path[lastIndex].index]
|
|
170
129
|
}
|
|
171
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Find the item in the hierarchy using the indices
|
|
133
|
+
*
|
|
134
|
+
* @param {Array<*>} items
|
|
135
|
+
* @param {Array<integer>} indices
|
|
136
|
+
* @param {import('@rokkit/core').FieldMapping} fields
|
|
137
|
+
* @returns {*}
|
|
138
|
+
*/
|
|
172
139
|
export function findItem(items, indices, fields) {
|
|
173
140
|
let item = items[indices[0]]
|
|
174
141
|
let levelFields = fields
|
package/src/index.js
CHANGED
package/src/lib/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './internal'
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { compact } from '@rokkit/core'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {Object} ActionHandlers
|
|
5
|
+
* @property {Function} [next]
|
|
6
|
+
* @property {Function} [previous]
|
|
7
|
+
* @property {Function} [select]
|
|
8
|
+
* @property {Function} [escape]
|
|
9
|
+
* @property {Function} [collapse]
|
|
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} [" "]
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Maps keyboard events to actions based on the given handlers and options.
|
|
33
|
+
*
|
|
34
|
+
* @param {ActionHandlers} handlers
|
|
35
|
+
* @param {NavigationOptions} options
|
|
36
|
+
* @returns {KeyboardActions}
|
|
37
|
+
*/
|
|
38
|
+
export function mapKeyboardEventsToActions(handlers, options) {
|
|
39
|
+
const { next, previous, select, escape } = handlers
|
|
40
|
+
const { horizontal, nested } = {
|
|
41
|
+
horizontal: false,
|
|
42
|
+
nested: false,
|
|
43
|
+
...options
|
|
44
|
+
}
|
|
45
|
+
let expand = nested ? handlers.expand : null
|
|
46
|
+
let collapse = nested ? handlers.collapse : null
|
|
47
|
+
|
|
48
|
+
return compact({
|
|
49
|
+
ArrowDown: horizontal ? expand : next,
|
|
50
|
+
ArrowUp: horizontal ? collapse : previous,
|
|
51
|
+
ArrowRight: horizontal ? next : expand,
|
|
52
|
+
ArrowLeft: horizontal ? previous : collapse,
|
|
53
|
+
Enter: select,
|
|
54
|
+
Escape: escape,
|
|
55
|
+
' ': select
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function getClosestAncestorWithAttribute(element, attribute) {
|
|
60
|
+
if (!element) return null
|
|
61
|
+
if (element.getAttribute(attribute)) return element
|
|
62
|
+
return getClosestAncestorWithAttribute(element.parentElement, attribute)
|
|
63
|
+
}
|
package/src/navigable.js
CHANGED
|
@@ -1,42 +1,47 @@
|
|
|
1
|
-
|
|
2
|
-
node,
|
|
3
|
-
{ horizontal = true, nested = false, enabled = true } = {}
|
|
4
|
-
) {
|
|
5
|
-
if (!enabled) return { destroy() {} }
|
|
6
|
-
const previous = () => node.dispatchEvent(new CustomEvent('previous'))
|
|
7
|
-
const next = () => node.dispatchEvent(new CustomEvent('next'))
|
|
8
|
-
const collapse = () => node.dispatchEvent(new CustomEvent('collapse'))
|
|
9
|
-
const expand = () => node.dispatchEvent(new CustomEvent('expand'))
|
|
10
|
-
const select = () => node.dispatchEvent(new CustomEvent('select'))
|
|
1
|
+
import { handleAction, getKeyboardActions } from './utils'
|
|
11
2
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
3
|
+
const defaultOptions = { horizontal: true, nested: false, enabled: true }
|
|
4
|
+
/**
|
|
5
|
+
* A svelte action function that captures keyboard evvents and emits event for corresponding movements.
|
|
6
|
+
*
|
|
7
|
+
* @param {HTMLElement} node
|
|
8
|
+
* @param {import('./types').NavigableOptions} options
|
|
9
|
+
* @returns {import('./types').SvelteActionReturn}
|
|
10
|
+
*/
|
|
11
|
+
export function navigable(node, options) {
|
|
12
|
+
options = { ...defaultOptions, ...options }
|
|
13
|
+
|
|
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'))
|
|
25
21
|
}
|
|
26
22
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
|
|
36
|
+
updateListeners(options)
|
|
36
37
|
|
|
37
38
|
return {
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
update: (config) => {
|
|
40
|
+
options = { ...options, ...config }
|
|
41
|
+
updateListeners(options)
|
|
42
|
+
},
|
|
43
|
+
destroy: () => {
|
|
44
|
+
updateListeners({ enabled: false })
|
|
40
45
|
}
|
|
41
46
|
}
|
|
42
47
|
}
|
package/src/navigator.js
CHANGED
|
@@ -1,87 +1,70 @@
|
|
|
1
|
-
import {
|
|
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
8
|
getCurrentNode
|
|
10
9
|
} from './hierarchy'
|
|
11
10
|
|
|
12
|
-
/**
|
|
13
|
-
* @typedef NavigatorOptions
|
|
14
|
-
* @property {Array<*>} items - An array containing the data set to navigate
|
|
15
|
-
* @property {boolean} [vertical=true] - Identifies whether navigation shoud be vertical or horizontal
|
|
16
|
-
* @property {string} [idPrefix='id-'] - id prefix used for identifying individual node
|
|
17
|
-
* @property {import('../constants').FieldMapping} fields - Field mapping to identify attributes to be used for state and identification of children
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
11
|
/**
|
|
21
12
|
* Keyboard navigation for Lists and NestedLists. The data is either nested or not and is not
|
|
22
13
|
* expected to switch from nested to simple list or vice-versa.
|
|
23
14
|
*
|
|
24
|
-
* @param {HTMLElement}
|
|
25
|
-
* @param {NavigatorOptions} options - Configuration options for the action
|
|
15
|
+
* @param {HTMLElement} element - Root element for the actionn
|
|
16
|
+
* @param {import('./types').NavigatorOptions} options - Configuration options for the action
|
|
26
17
|
* @returns
|
|
27
18
|
*/
|
|
28
|
-
export function navigator(
|
|
19
|
+
export function navigator(element, options) {
|
|
29
20
|
const { fields, enabled = true, vertical = true, idPrefix = 'id-' } = options
|
|
30
21
|
let items, path, currentNode
|
|
31
22
|
|
|
32
23
|
if (!enabled) return { destroy: () => {} }
|
|
33
24
|
|
|
25
|
+
// todo: Update should handle selection value change
|
|
26
|
+
// should we wait a tick before updating?
|
|
34
27
|
const update = (options) => {
|
|
28
|
+
const previousNode = currentNode
|
|
35
29
|
items = options.items
|
|
36
30
|
path = pathFromIndices(options.indices ?? [], items, fields)
|
|
37
31
|
currentNode = getCurrentNode(path)
|
|
32
|
+
|
|
33
|
+
if (previousNode !== currentNode && currentNode) {
|
|
34
|
+
const indices = indicesFromPath(path)
|
|
35
|
+
let current = element.querySelector('#' + idPrefix + indices.join('-'))
|
|
36
|
+
if (current) {
|
|
37
|
+
current.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
|
|
38
|
+
}
|
|
39
|
+
}
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
const next = () => {
|
|
41
43
|
const previousNode = currentNode
|
|
42
44
|
path = moveNext(path, items, fields)
|
|
43
45
|
currentNode = getCurrentNode(path)
|
|
44
|
-
|
|
45
46
|
if (previousNode !== currentNode)
|
|
46
|
-
moveTo(
|
|
47
|
+
moveTo(element, path, currentNode, idPrefix)
|
|
47
48
|
}
|
|
49
|
+
|
|
48
50
|
const previous = () => {
|
|
49
51
|
const previousNode = currentNode
|
|
50
52
|
path = movePrevious(path)
|
|
51
53
|
if (path.length > 0) {
|
|
52
54
|
currentNode = getCurrentNode(path)
|
|
53
55
|
if (previousNode !== currentNode)
|
|
54
|
-
moveTo(
|
|
56
|
+
moveTo(element, path, currentNode, idPrefix)
|
|
55
57
|
}
|
|
56
58
|
}
|
|
57
59
|
const select = () => {
|
|
58
|
-
if (currentNode)
|
|
59
|
-
node.dispatchEvent(
|
|
60
|
-
new CustomEvent('select', {
|
|
61
|
-
detail: {
|
|
62
|
-
path: indicesFromPath(path),
|
|
63
|
-
node: currentNode,
|
|
64
|
-
id: getId(currentNode, fields)
|
|
65
|
-
}
|
|
66
|
-
})
|
|
67
|
-
)
|
|
60
|
+
if (currentNode) emit('select', element, indicesFromPath(path), currentNode)
|
|
68
61
|
}
|
|
69
62
|
const collapse = () => {
|
|
70
63
|
if (currentNode) {
|
|
71
|
-
const collapse =
|
|
72
|
-
hasChildren(currentNode, path[path.length - 1].fields) &&
|
|
73
|
-
currentNode[path[path.length - 1].fields.isOpen]
|
|
64
|
+
const collapse = isExpanded(currentNode, path[path.length - 1].fields)
|
|
74
65
|
if (collapse) {
|
|
75
66
|
currentNode[path[path.length - 1].fields.isOpen] = false
|
|
76
|
-
|
|
77
|
-
new CustomEvent('collapse', {
|
|
78
|
-
detail: {
|
|
79
|
-
path: indicesFromPath(path),
|
|
80
|
-
node: currentNode,
|
|
81
|
-
id: getId(currentNode, fields)
|
|
82
|
-
}
|
|
83
|
-
})
|
|
84
|
-
)
|
|
67
|
+
emit('collapse', element, indicesFromPath(path), currentNode)
|
|
85
68
|
} else if (path.length > 0) {
|
|
86
69
|
path = path.slice(0, -1)
|
|
87
70
|
currentNode = getCurrentNode(path)
|
|
@@ -92,41 +75,20 @@ export function navigator(node, options) {
|
|
|
92
75
|
const expand = () => {
|
|
93
76
|
if (currentNode && hasChildren(currentNode, path[path.length - 1].fields)) {
|
|
94
77
|
currentNode[path[path.length - 1].fields.isOpen] = true
|
|
95
|
-
|
|
96
|
-
new CustomEvent('expand', {
|
|
97
|
-
detail: {
|
|
98
|
-
path: indicesFromPath(path),
|
|
99
|
-
node: currentNode,
|
|
100
|
-
id: getId(currentNode, fields)
|
|
101
|
-
}
|
|
102
|
-
})
|
|
103
|
-
)
|
|
78
|
+
emit('expand', element, indicesFromPath(path), currentNode)
|
|
104
79
|
}
|
|
105
80
|
}
|
|
81
|
+
const handlers = { next, previous, select, collapse, expand }
|
|
106
82
|
|
|
107
83
|
update(options)
|
|
108
84
|
|
|
109
85
|
const nested = isNested(items, fields)
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const states = !nested
|
|
114
|
-
? {}
|
|
115
|
-
: vertical
|
|
116
|
-
? { ArrowRight: expand, ArrowLeft: collapse }
|
|
117
|
-
: { ArrowDown: expand, ArrowUp: collapse }
|
|
118
|
-
const actions = { ...movement, Enter: select, ...states }
|
|
119
|
-
|
|
120
|
-
const handleKeyDown = (event) => {
|
|
121
|
-
if (actions[event.key]) {
|
|
122
|
-
event.preventDefault()
|
|
123
|
-
event.stopPropagation()
|
|
124
|
-
actions[event.key]()
|
|
125
|
-
}
|
|
126
|
-
}
|
|
86
|
+
const actions = mapKeyboardEventsToActions(vertical, nested, handlers)
|
|
87
|
+
|
|
88
|
+
const handleKeyDown = (event) => handleAction(actions, event)
|
|
127
89
|
|
|
128
90
|
const handleClick = (event) => {
|
|
129
|
-
let target = findParentWithDataPath(event.target)
|
|
91
|
+
let target = findParentWithDataPath(event.target, element)
|
|
130
92
|
let indices = !target
|
|
131
93
|
? []
|
|
132
94
|
: target.dataset.path
|
|
@@ -143,65 +105,96 @@ export function navigator(node, options) {
|
|
|
143
105
|
const event = currentNode[path[path.length - 1].fields.isOpen]
|
|
144
106
|
? 'expand'
|
|
145
107
|
: 'collapse'
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
detail: {
|
|
149
|
-
path: indices,
|
|
150
|
-
node: currentNode,
|
|
151
|
-
id: getId(currentNode, fields)
|
|
152
|
-
}
|
|
153
|
-
})
|
|
154
|
-
)
|
|
155
|
-
}
|
|
156
|
-
node.dispatchEvent(
|
|
157
|
-
new CustomEvent('select', {
|
|
158
|
-
detail: {
|
|
159
|
-
path: indices,
|
|
160
|
-
node: currentNode,
|
|
161
|
-
id: getId(currentNode, fields)
|
|
162
|
-
}
|
|
163
|
-
})
|
|
164
|
-
)
|
|
108
|
+
emit(event, element, indices, currentNode)
|
|
109
|
+
} else if (currentNode) emit('select', element, indices, currentNode)
|
|
165
110
|
}
|
|
166
111
|
}
|
|
167
112
|
|
|
168
|
-
|
|
169
|
-
|
|
113
|
+
element.addEventListener('keydown', handleKeyDown)
|
|
114
|
+
element.addEventListener('click', handleClick)
|
|
170
115
|
|
|
171
116
|
return {
|
|
172
117
|
update,
|
|
173
118
|
destroy() {
|
|
174
|
-
|
|
175
|
-
|
|
119
|
+
element.removeEventListener('keydown', handleKeyDown)
|
|
120
|
+
element.removeEventListener('click', handleClick)
|
|
176
121
|
}
|
|
177
122
|
}
|
|
178
123
|
}
|
|
179
124
|
|
|
180
|
-
|
|
125
|
+
/**
|
|
126
|
+
* Move to the element with the given path
|
|
127
|
+
*
|
|
128
|
+
* @param {HTMLElement} element
|
|
129
|
+
* @param {*} path
|
|
130
|
+
* @param {*} currentNode
|
|
131
|
+
* @param {*} idPrefix
|
|
132
|
+
*/
|
|
133
|
+
export function moveTo(element, path, currentNode, idPrefix) {
|
|
181
134
|
const indices = indicesFromPath(path)
|
|
182
|
-
|
|
183
|
-
let current = node.querySelector('#' + idPrefix + indices.join('-'))
|
|
135
|
+
let current = element.querySelector('#' + idPrefix + indices.join('-'))
|
|
184
136
|
if (current) current.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
|
|
185
137
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
138
|
+
emit('move', element, indices, currentNode)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Find the parent element with data-path attribute
|
|
143
|
+
*
|
|
144
|
+
* @param {HTMLElement} element
|
|
145
|
+
* @param {HTMLElement} root
|
|
146
|
+
* @returns {HTMLElement}
|
|
147
|
+
*/
|
|
148
|
+
export function findParentWithDataPath(element, root) {
|
|
149
|
+
if (element.hasAttribute('data-path')) return element
|
|
150
|
+
let parent = element.parentNode
|
|
151
|
+
|
|
152
|
+
while (parent && parent !== root && !parent.hasAttribute('data-path')) {
|
|
153
|
+
parent = parent.parentNode
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return parent !== root ? parent : null
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Emit a custom event on the element with the path and node as detail
|
|
161
|
+
*
|
|
162
|
+
* @param {string} event
|
|
163
|
+
* @param {HTMLElement} element
|
|
164
|
+
* @param {Array<integer>} indices
|
|
165
|
+
* @param {*} node
|
|
166
|
+
*/
|
|
167
|
+
function emit(event, element, indices, node) {
|
|
168
|
+
element.dispatchEvent(
|
|
169
|
+
new CustomEvent(event, {
|
|
189
170
|
detail: {
|
|
190
171
|
path: indices,
|
|
191
|
-
node:
|
|
192
|
-
id
|
|
172
|
+
node: node
|
|
193
173
|
}
|
|
194
174
|
})
|
|
195
175
|
)
|
|
196
176
|
}
|
|
197
177
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
let parent = element.parentNode
|
|
178
|
+
function mapKeyboardEventsToActions(vertical, nested, handlers) {
|
|
179
|
+
let actions = { Enter: handlers.select }
|
|
201
180
|
|
|
202
|
-
|
|
203
|
-
|
|
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
|
+
}
|
|
204
197
|
}
|
|
205
198
|
|
|
206
|
-
return
|
|
199
|
+
return actions
|
|
207
200
|
}
|
package/src/pannable.js
CHANGED
package/src/swipeable.js
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
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
|
+
|
|
1
9
|
export function swipeable(
|
|
2
10
|
node,
|
|
3
11
|
{
|
|
@@ -8,8 +16,7 @@ export function swipeable(
|
|
|
8
16
|
minSpeed = 300
|
|
9
17
|
} = {}
|
|
10
18
|
) {
|
|
11
|
-
|
|
12
|
-
|
|
19
|
+
let listening = false
|
|
13
20
|
let startX
|
|
14
21
|
let startY
|
|
15
22
|
let startTime
|
|
@@ -49,17 +56,37 @@ export function swipeable(
|
|
|
49
56
|
}
|
|
50
57
|
}
|
|
51
58
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
+
const updateListeners = (enabled) => {
|
|
60
|
+
if (enabled && !listening) {
|
|
61
|
+
node.addEventListener('touchstart', touchStart)
|
|
62
|
+
node.addEventListener('touchend', touchEnd)
|
|
63
|
+
node.addEventListener('mousedown', touchStart)
|
|
64
|
+
node.addEventListener('mouseup', touchEnd)
|
|
65
|
+
listening = true
|
|
66
|
+
}
|
|
67
|
+
if (!enabled && listening) {
|
|
59
68
|
node.removeEventListener('touchstart', touchStart)
|
|
60
69
|
node.removeEventListener('touchend', touchEnd)
|
|
61
70
|
node.removeEventListener('mousedown', touchStart)
|
|
62
71
|
node.removeEventListener('mouseup', touchEnd)
|
|
72
|
+
listening = false
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
updateListeners(enabled)
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
update: (options) => {
|
|
80
|
+
horizontal = options.horizontal
|
|
81
|
+
vertical = options.vertical
|
|
82
|
+
threshold = options.threshold
|
|
83
|
+
enabled = options.enabled
|
|
84
|
+
minSpeed = options.minSpeed
|
|
85
|
+
|
|
86
|
+
updateListeners(enabled)
|
|
87
|
+
},
|
|
88
|
+
destroy() {
|
|
89
|
+
updateListeners(false)
|
|
63
90
|
}
|
|
64
91
|
}
|
|
65
92
|
}
|
package/src/themeable.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { theme } from '@rokkit/stores'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* A svelte action function that adds theme classes to the element
|
|
5
5
|
*
|
|
6
6
|
* @param {HTMLElement} node
|
|
7
7
|
*/
|
|
@@ -16,6 +16,14 @@ export function themable(node) {
|
|
|
16
16
|
})
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Switch the class on the node
|
|
21
|
+
*
|
|
22
|
+
* @param {HTMLElement} node
|
|
23
|
+
* @param {string} current
|
|
24
|
+
* @param {string} previous
|
|
25
|
+
* @returns
|
|
26
|
+
*/
|
|
19
27
|
function switchClass(node, current, previous) {
|
|
20
28
|
if (current && current !== previous) {
|
|
21
29
|
node.classList.remove(previous)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getClosestAncestorWithAttribute,
|
|
3
|
+
mapKeyboardEventsToActions
|
|
4
|
+
} from './lib'
|
|
5
|
+
|
|
6
|
+
const defaultOptions = {
|
|
7
|
+
horizontal: false,
|
|
8
|
+
nested: false,
|
|
9
|
+
enabled: true
|
|
10
|
+
}
|
|
11
|
+
export function traversable(element, data) {
|
|
12
|
+
let listening = false
|
|
13
|
+
let options = {}
|
|
14
|
+
let tracker = {}
|
|
15
|
+
let handlers = {}
|
|
16
|
+
|
|
17
|
+
let actions = {
|
|
18
|
+
next: () => emit(element, 'move', tracker),
|
|
19
|
+
previous: () => emit(element, 'move', tracker),
|
|
20
|
+
select: () => emit(element, 'select', tracker),
|
|
21
|
+
escape: () => emit(element, 'escape', tracker),
|
|
22
|
+
collapse: () => emit(element, 'collapse', tracker),
|
|
23
|
+
expand: () => emit(element, 'expand', tracker)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let listeners = {
|
|
27
|
+
keydown: (event) => {
|
|
28
|
+
const action = handlers[event.key]
|
|
29
|
+
if (action) action(event)
|
|
30
|
+
},
|
|
31
|
+
click: (event) => {
|
|
32
|
+
const target = getClosestAncestorWithAttribute(event.target, 'data-index')
|
|
33
|
+
|
|
34
|
+
if (target) {
|
|
35
|
+
const index = parseInt(target.getAttribute('data-index'))
|
|
36
|
+
tracker.index = index
|
|
37
|
+
actions.select()
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const configure = (data) => {
|
|
43
|
+
// const valueChanged = options.value !== data.value
|
|
44
|
+
options = { ...options, ...data }
|
|
45
|
+
|
|
46
|
+
listening = setupEventHandlers(element, options, listening, listeners)
|
|
47
|
+
handlers = mapKeyboardEventsToActions(actions, options)
|
|
48
|
+
// if (valueChanged) handleValueChange(element, options)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
configure({ ...defaultOptions, ...data })
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
update: configure,
|
|
55
|
+
destroy: () => configure({ enabled: false })
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function setupEventHandlers(element, options, listening, handlers) {
|
|
60
|
+
const { enabled } = options
|
|
61
|
+
|
|
62
|
+
if (enabled && !listening) {
|
|
63
|
+
Object.entries(handlers).forEach(([event, handler]) =>
|
|
64
|
+
element.addEventListener(event, handler)
|
|
65
|
+
)
|
|
66
|
+
} else if (!enabled && listening) {
|
|
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) {}
|
package/src/types.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef SvelteActionReturn
|
|
3
|
+
* @property {() => void} destroy
|
|
4
|
+
* @property {() => void} [update]
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef FillableData
|
|
9
|
+
* @property {string} value
|
|
10
|
+
* @property {integer} actualIndex
|
|
11
|
+
* @property {integer} expectedIndex
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef FillOptions
|
|
16
|
+
* @property {Array<FillableData>} options available options to fill
|
|
17
|
+
* @property {integer} current index of option to be filled
|
|
18
|
+
* @property {boolean} check validate filled values
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A part of the path to node in hierarchy
|
|
23
|
+
*
|
|
24
|
+
* @typedef PathFragment
|
|
25
|
+
* @property {integer} index - Index to item in array
|
|
26
|
+
* @property {Array<*>} items - Array of items
|
|
27
|
+
* @property {import('@rokkit/core').FieldMapping} fields - Field mapping for the data
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Options for the Navigable action
|
|
32
|
+
* @typedef NavigableOptions
|
|
33
|
+
* @property {boolean} horizontal - Navigate horizontally
|
|
34
|
+
* @property {boolean} nested - Navigate nested items
|
|
35
|
+
* @property {boolean} enabled - Enable navigation
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @typedef NavigatorOptions
|
|
40
|
+
* @property {Array<*>} items - An array containing the data set to navigate
|
|
41
|
+
* @property {boolean} [vertical=true] - Identifies whether navigation shoud be vertical or horizontal
|
|
42
|
+
* @property {string} [idPrefix='id-'] - id prefix used for identifying individual node
|
|
43
|
+
* @property {import('../constants').FieldMapping} fields - Field mapping to identify attributes to be used for state and identification of children
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @typedef SwipeableOptions
|
|
48
|
+
* @property {boolean} horizontal - Swipe horizontally
|
|
49
|
+
* @property {boolean} vertical - Swipe vertically
|
|
50
|
+
* @property {boolean} enabled - Enable swiping
|
|
51
|
+
* @property {number} threshold - Threshold for swipe
|
|
52
|
+
* @property {number} minSpeed - Minimum speed for swipe
|
|
53
|
+
*/
|
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
|
+
}
|