@rokkit/actions 1.0.0-next.36 → 1.0.0-next.37

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.37",
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.37"
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,17 +1,8 @@
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
1
  /**
11
2
  * Check if the current item is a parent
12
3
  *
13
4
  * @param {*} item
14
- * @param {import('../constants').FieldMapping} fields
5
+ * @param {import('@rokkit/core').FieldMapping} fields
15
6
  * @returns {boolean}
16
7
  */
17
8
  export function hasChildren(item, fields) {
@@ -26,7 +17,7 @@ export function hasChildren(item, fields) {
26
17
  * Check if the current item is a parent and is expanded
27
18
  *
28
19
  * @param {*} item
29
- * @param {import('../constants').FieldMapping} fields
20
+ * @param {import('@rokkit/core').FieldMapping} fields
30
21
  * @returns {boolean}
31
22
  */
32
23
  export function isExpanded(item, fields) {
@@ -42,7 +33,7 @@ export function isExpanded(item, fields) {
42
33
  * Verify if at least one item has children
43
34
  *
44
35
  * @param {Array<*>} items
45
- * @param {import('../constants').FieldMapping} fields
36
+ * @param {import('@rokkit/core').FieldMapping} fields
46
37
  * @returns {boolean}
47
38
  */
48
39
  export function isNested(items, fields) {
@@ -55,7 +46,7 @@ export function isNested(items, fields) {
55
46
  /**
56
47
  * Navigate to last visible child in the hirarchy starting with the provided path
57
48
  *
58
- * @param {Array<PathFragment>} path - path to a node in the hierarchy
49
+ * @param {Array<import('./types').PathFragment>} path - path to a node in the hierarchy
59
50
  * @returns
60
51
  */
61
52
  export function navigateToLastVisibleChild(path) {
@@ -78,9 +69,9 @@ export function navigateToLastVisibleChild(path) {
78
69
  /**
79
70
  * Navigate to the next item
80
71
  *
81
- * @param {Array<PathFragment>} path - path to a node in the hierarchy
72
+ * @param {Array<import('./types').PathFragment>} path - path to a node in the hierarchy
82
73
  * @param {Array<*>} items - array of items
83
- * @param {import('../constants').FieldMapping} fields - field mapping
74
+ * @param {import('@rokkit/core').FieldMapping} fields - field mapping
84
75
  * @returns
85
76
  */
86
77
  export function moveNext(path, items, fields) {
@@ -115,7 +106,7 @@ export function moveNext(path, items, fields) {
115
106
  /**
116
107
  * Navigate to the previous item
117
108
  *
118
- * @param {Array<PathFragment>} path - path to a node in the hierarchy
109
+ * @param {Array<import('./types').PathFragment>} path - path to a node in the hierarchy
119
110
  * @returns
120
111
  */
121
112
  export function movePrevious(path) {
@@ -139,7 +130,7 @@ export function movePrevious(path) {
139
130
  *
140
131
  * @param {Array<integer>} indices
141
132
  * @param {Array<*>} items
142
- * @param {import('../constants').FieldMapping} fields
133
+ * @param {import('@rokkit/core').FieldMapping} fields
143
134
  * @returns
144
135
  */
145
136
  export function pathFromIndices(indices, items, fields) {
@@ -160,15 +151,34 @@ export function pathFromIndices(indices, items, fields) {
160
151
  return path
161
152
  }
162
153
 
154
+ /**
155
+ * Get the indices from the path
156
+ * @param {Array<import('./types').PathFragment>} path
157
+ * @returns {Array<integer>}
158
+ */
163
159
  export function indicesFromPath(path) {
164
160
  return path.map(({ index }) => index)
165
161
  }
162
+
163
+ /**
164
+ * Get the current node from the path
165
+ * @param {Array<import('./types').PathFragment>} path
166
+ * @returns {*}
167
+ */
166
168
  export function getCurrentNode(path) {
167
169
  if (path.length === 0) return null
168
170
  const lastIndex = path.length - 1
169
171
  return path[lastIndex].items[path[lastIndex].index]
170
172
  }
171
173
 
174
+ /**
175
+ * Find the item in the hierarchy using the indices
176
+ *
177
+ * @param {Array<*>} items
178
+ * @param {Array<integer>} indices
179
+ * @param {import('@rokkit/core').FieldMapping} fields
180
+ * @returns {*}
181
+ */
172
182
  export function findItem(items, indices, fields) {
173
183
  let item = items[indices[0]]
174
184
  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'
package/src/navigable.js CHANGED
@@ -1,3 +1,10 @@
1
+ /**
2
+ * A svelte action function that captures keyboard evvents and emits event for corresponding movements.
3
+ *
4
+ * @param {HTMLElement} node
5
+ * @param {import('./types').NavigableOptions} options
6
+ * @returns {import('./types').SvelteActionReturn}
7
+ */
1
8
  export function navigable(
2
9
  node,
3
10
  { horizontal = true, nested = false, enabled = true } = {}
package/src/navigator.js CHANGED
@@ -1,4 +1,4 @@
1
- import { getId } from '@rokkit/core'
1
+ // import { tick } from 'svelte'
2
2
  import {
3
3
  moveNext,
4
4
  movePrevious,
@@ -6,82 +6,67 @@ import {
6
6
  hasChildren,
7
7
  pathFromIndices,
8
8
  indicesFromPath,
9
- getCurrentNode
9
+ getCurrentNode,
10
+ isExpanded
10
11
  } from './hierarchy'
11
12
 
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
13
  /**
21
14
  * Keyboard navigation for Lists and NestedLists. The data is either nested or not and is not
22
15
  * expected to switch from nested to simple list or vice-versa.
23
16
  *
24
- * @param {HTMLElement} node - The node on which the action is to be used on
25
- * @param {NavigatorOptions} options - Configuration options for the action
17
+ * @param {HTMLElement} element - Root element for the actionn
18
+ * @param {import('./types').NavigatorOptions} options - Configuration options for the action
26
19
  * @returns
27
20
  */
28
- export function navigator(node, options) {
21
+ export function navigator(element, options) {
29
22
  const { fields, enabled = true, vertical = true, idPrefix = 'id-' } = options
30
23
  let items, path, currentNode
31
24
 
32
25
  if (!enabled) return { destroy: () => {} }
33
26
 
27
+ // todo: Update should handle selection value change
28
+ // should we wait a tick before updating?
34
29
  const update = (options) => {
30
+ const previousNode = currentNode
35
31
  items = options.items
36
32
  path = pathFromIndices(options.indices ?? [], items, fields)
37
33
  currentNode = getCurrentNode(path)
34
+
35
+ if (previousNode !== currentNode && currentNode) {
36
+ const indices = indicesFromPath(path)
37
+ let current = element.querySelector('#' + idPrefix + indices.join('-'))
38
+ if (current) {
39
+ current.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
40
+ }
41
+ }
38
42
  }
39
43
 
40
44
  const next = () => {
41
45
  const previousNode = currentNode
42
46
  path = moveNext(path, items, fields)
43
47
  currentNode = getCurrentNode(path)
44
-
45
48
  if (previousNode !== currentNode)
46
- moveTo(node, path, currentNode, idPrefix, fields)
49
+ moveTo(element, path, currentNode, idPrefix)
47
50
  }
51
+
48
52
  const previous = () => {
49
53
  const previousNode = currentNode
50
54
  path = movePrevious(path)
51
55
  if (path.length > 0) {
52
56
  currentNode = getCurrentNode(path)
53
57
  if (previousNode !== currentNode)
54
- moveTo(node, path, currentNode, idPrefix, fields)
58
+ moveTo(element, path, currentNode, idPrefix)
55
59
  }
56
60
  }
57
61
  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
- )
62
+ if (currentNode) emit('select', element, indicesFromPath(path), currentNode)
68
63
  }
69
64
  const collapse = () => {
70
65
  if (currentNode) {
71
- const collapse =
72
- hasChildren(currentNode, path[path.length - 1].fields) &&
73
- currentNode[path[path.length - 1].fields.isOpen]
66
+ const collapse = isExpanded(currentNode, path[path.length - 1].fields)
74
67
  if (collapse) {
75
68
  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
- )
69
+ emit('collapse', element, indicesFromPath(path), currentNode)
85
70
  } else if (path.length > 0) {
86
71
  path = path.slice(0, -1)
87
72
  currentNode = getCurrentNode(path)
@@ -92,15 +77,7 @@ export function navigator(node, options) {
92
77
  const expand = () => {
93
78
  if (currentNode && hasChildren(currentNode, path[path.length - 1].fields)) {
94
79
  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
- )
80
+ emit('expand', element, indicesFromPath(path), currentNode)
104
81
  }
105
82
  }
106
83
 
@@ -126,7 +103,7 @@ export function navigator(node, options) {
126
103
  }
127
104
 
128
105
  const handleClick = (event) => {
129
- let target = findParentWithDataPath(event.target)
106
+ let target = findParentWithDataPath(event.target, element)
130
107
  let indices = !target
131
108
  ? []
132
109
  : target.dataset.path
@@ -143,65 +120,72 @@ export function navigator(node, options) {
143
120
  const event = currentNode[path[path.length - 1].fields.isOpen]
144
121
  ? 'expand'
145
122
  : '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
- )
123
+ emit(event, element, indices, currentNode)
124
+ } else if (currentNode) emit('select', element, indices, currentNode)
165
125
  }
166
126
  }
167
127
 
168
- node.addEventListener('keydown', handleKeyDown)
169
- node.addEventListener('click', handleClick)
128
+ element.addEventListener('keydown', handleKeyDown)
129
+ element.addEventListener('click', handleClick)
170
130
 
171
131
  return {
172
132
  update,
173
133
  destroy() {
174
- node.removeEventListener('keydown', handleKeyDown)
175
- node.removeEventListener('click', handleClick)
134
+ element.removeEventListener('keydown', handleKeyDown)
135
+ element.removeEventListener('click', handleClick)
176
136
  }
177
137
  }
178
138
  }
179
139
 
180
- export function moveTo(node, path, currentNode, idPrefix, fields) {
140
+ /**
141
+ * Move to the element with the given path
142
+ *
143
+ * @param {HTMLElement} element
144
+ * @param {*} path
145
+ * @param {*} currentNode
146
+ * @param {*} idPrefix
147
+ */
148
+ export function moveTo(element, path, currentNode, idPrefix) {
181
149
  const indices = indicesFromPath(path)
182
-
183
- let current = node.querySelector('#' + idPrefix + indices.join('-'))
150
+ let current = element.querySelector('#' + idPrefix + indices.join('-'))
184
151
  if (current) current.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
185
152
 
186
- const id = getId(currentNode, fields)
187
- node.dispatchEvent(
188
- new CustomEvent('move', {
189
- detail: {
190
- path: indices,
191
- node: currentNode,
192
- id
193
- }
194
- })
195
- )
153
+ emit('move', element, indices, currentNode)
196
154
  }
197
155
 
198
- export function findParentWithDataPath(element) {
156
+ /**
157
+ * Find the parent element with data-path attribute
158
+ *
159
+ * @param {HTMLElement} element
160
+ * @param {HTMLElement} root
161
+ * @returns {HTMLElement}
162
+ */
163
+ export function findParentWithDataPath(element, root) {
199
164
  if (element.hasAttribute('data-path')) return element
200
165
  let parent = element.parentNode
201
166
 
202
- while (parent && !parent.hasAttribute('data-path')) {
167
+ while (parent && parent !== root && !parent.hasAttribute('data-path')) {
203
168
  parent = parent.parentNode
204
169
  }
205
170
 
206
- return parent
171
+ return parent !== root ? parent : null
172
+ }
173
+
174
+ /**
175
+ * Emit a custom event on the element with the path and node as detail
176
+ *
177
+ * @param {string} event
178
+ * @param {HTMLElement} element
179
+ * @param {Array<integer>} indices
180
+ * @param {*} node
181
+ */
182
+ function emit(event, element, indices, node) {
183
+ element.dispatchEvent(
184
+ new CustomEvent(event, {
185
+ detail: {
186
+ path: indices,
187
+ node: node
188
+ }
189
+ })
190
+ )
207
191
  }
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
  {
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)
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
+ */