@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rokkit/actions",
3
- "version": "1.0.0-next.36",
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.2",
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.0.5",
21
+ "svelte": "^4.1.1",
22
22
  "typescript": "^5.1.6",
23
23
  "validators": "latest",
24
- "vite": "^4.4.4",
24
+ "vite": "^4.4.7",
25
25
  "vitest": "~0.33.0",
26
- "shared-config": "1.0.0-next.36"
26
+ "shared-config": "1.0.0-next.38"
27
27
  },
28
28
  "files": [
29
29
  "src/**/*.js",
@@ -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 {*} node
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 {*} blanks
47
- * @param {*} click
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 {*} blanks
62
- * @param {*} options
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 {*} event
80
- * @param {*} node
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 {*} blanks
102
- * @param {*} data
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('../constants').FieldMapping} fields - field mapping
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('../constants').FieldMapping} fields
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
@@ -1,3 +1,4 @@
1
+ import './types'
1
2
  export { fillable } from './fillable'
2
3
  export { pannable } from './pannable'
3
4
  export { navigable } from './navigable'
@@ -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
- export function navigable(
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
- const movement = horizontal
13
- ? { ArrowLeft: previous, ArrowRight: next }
14
- : { ArrowUp: previous, ArrowDown: next }
15
- const change = nested
16
- ? horizontal
17
- ? { ArrowUp: collapse, ArrowDown: expand }
18
- : { ArrowLeft: collapse, ArrowRight: expand }
19
- : {}
20
- const actions = {
21
- Enter: select,
22
- ' ': select,
23
- ...movement,
24
- ...change
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
- function handleKeydown(event) {
28
- if (actions[event.key]) {
29
- event.preventDefault()
30
- event.stopPropagation()
31
- actions[event.key]()
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
- node.addEventListener('keydown', handleKeydown)
36
+ updateListeners(options)
36
37
 
37
38
  return {
38
- destroy() {
39
- node.removeEventListener('keydown', handleKeydown)
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 { getId } from '@rokkit/core'
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} node - The node on which the action is to be used on
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(node, options) {
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(node, path, currentNode, idPrefix, fields)
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(node, path, currentNode, idPrefix, fields)
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
- node.dispatchEvent(
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
- node.dispatchEvent(
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 movement = vertical
111
- ? { ArrowDown: next, ArrowUp: previous }
112
- : { ArrowRight: next, ArrowLeft: previous }
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
- node.dispatchEvent(
147
- new CustomEvent(event, {
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
- node.addEventListener('keydown', handleKeyDown)
169
- node.addEventListener('click', handleClick)
113
+ element.addEventListener('keydown', handleKeyDown)
114
+ element.addEventListener('click', handleClick)
170
115
 
171
116
  return {
172
117
  update,
173
118
  destroy() {
174
- node.removeEventListener('keydown', handleKeyDown)
175
- node.removeEventListener('click', handleClick)
119
+ element.removeEventListener('keydown', handleKeyDown)
120
+ element.removeEventListener('click', handleClick)
176
121
  }
177
122
  }
178
123
  }
179
124
 
180
- export function moveTo(node, path, currentNode, idPrefix, fields) {
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
- const id = getId(currentNode, fields)
187
- node.dispatchEvent(
188
- new CustomEvent('move', {
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: currentNode,
192
- id
172
+ node: node
193
173
  }
194
174
  })
195
175
  )
196
176
  }
197
177
 
198
- export function findParentWithDataPath(element) {
199
- if (element.hasAttribute('data-path')) return element
200
- let parent = element.parentNode
178
+ function mapKeyboardEventsToActions(vertical, nested, handlers) {
179
+ let actions = { Enter: handlers.select }
201
180
 
202
- while (parent && !parent.hasAttribute('data-path')) {
203
- parent = parent.parentNode
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 parent
199
+ return actions
207
200
  }
package/src/pannable.js CHANGED
@@ -1,9 +1,8 @@
1
- // pannable.js
2
1
  /**
3
2
  * Handle drag and move events
4
3
  *
5
- * @param {*} node
6
- * @returns
4
+ * @param {HTMLElement} node
5
+ * @returns {import('./types').SvelteActionReturn}
7
6
  */
8
7
  export function pannable(node) {
9
8
  let x
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
- if (!enabled) return { destroy() {} }
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
- node.addEventListener('touchstart', touchStart)
53
- node.addEventListener('touchend', touchEnd)
54
- node.addEventListener('mousedown', touchStart)
55
- node.addEventListener('mouseup', touchEnd)
56
-
57
- return {
58
- destroy() {
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
- * Sets theme level classes based on the theme store
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
+ }