@rokkit/actions 1.0.0-next.35 → 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.35",
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
- "@vitest/coverage-v8": "^0.32.3",
19
- "@vitest/ui": "~0.32.3",
18
+ "@vitest/coverage-v8": "^0.33.0",
19
+ "@vitest/ui": "~0.33.0",
20
20
  "jsdom": "^22.1.0",
21
- "svelte": "^4.0.1",
21
+ "svelte": "^4.1.1",
22
22
  "typescript": "^5.1.6",
23
23
  "validators": "latest",
24
- "vite": "^4.3.9",
25
- "vitest": "~0.32.3",
26
- "shared-config": "1.0.0-next.35"
24
+ "vite": "^4.4.7",
25
+ "vitest": "~0.33.0",
26
+ "shared-config": "1.0.0-next.37"
27
27
  },
28
28
  "files": [
29
29
  "src/**/*.js",
@@ -40,6 +40,7 @@
40
40
  }
41
41
  },
42
42
  "dependencies": {
43
+ "@rokkit/core": "latest",
43
44
  "@rokkit/stores": "latest"
44
45
  },
45
46
  "scripts": {
@@ -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,12 +46,12 @@ 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) {
62
53
  let current = path[path.length - 1]
63
- // console.log(current)
54
+
64
55
  while (isExpanded(current.items[current.index], current.fields)) {
65
56
  const items = current.items[current.index][current.fields.children]
66
57
  const level = {
@@ -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,3 +1,4 @@
1
+ // import { tick } from 'svelte'
1
2
  import {
2
3
  moveNext,
3
4
  movePrevious,
@@ -5,72 +6,67 @@ import {
5
6
  hasChildren,
6
7
  pathFromIndices,
7
8
  indicesFromPath,
8
- getCurrentNode
9
+ getCurrentNode,
10
+ isExpanded
9
11
  } from './hierarchy'
10
- /**
11
- * @typedef NavigatorOptions
12
- * @property {Array<*>} items - An array containing the data set to navigate
13
- * @property {boolean} [vertical=true] - Identifies whether navigation shoud be vertical or horizontal
14
- * @property {string} [idPrefix='id-'] - id prefix used for identifying individual node
15
- * @property {import('../constants').FieldMapping} fields - Field mapping to identify attributes to be used for state and identification of children
16
- */
17
12
 
18
13
  /**
19
14
  * Keyboard navigation for Lists and NestedLists. The data is either nested or not and is not
20
15
  * expected to switch from nested to simple list or vice-versa.
21
16
  *
22
- * @param {HTMLElement} node - The node on which the action is to be used on
23
- * @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
24
19
  * @returns
25
20
  */
26
- export function navigator(node, options) {
21
+ export function navigator(element, options) {
27
22
  const { fields, enabled = true, vertical = true, idPrefix = 'id-' } = options
28
23
  let items, path, currentNode
29
24
 
30
25
  if (!enabled) return { destroy: () => {} }
31
26
 
27
+ // todo: Update should handle selection value change
28
+ // should we wait a tick before updating?
32
29
  const update = (options) => {
30
+ const previousNode = currentNode
33
31
  items = options.items
34
32
  path = pathFromIndices(options.indices ?? [], items, fields)
35
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
+ }
36
42
  }
37
43
 
38
44
  const next = () => {
39
45
  const previousNode = currentNode
40
46
  path = moveNext(path, items, fields)
41
47
  currentNode = getCurrentNode(path)
42
-
43
- if (previousNode !== currentNode) moveTo(node, path, currentNode, idPrefix)
48
+ if (previousNode !== currentNode)
49
+ moveTo(element, path, currentNode, idPrefix)
44
50
  }
51
+
45
52
  const previous = () => {
46
53
  const previousNode = currentNode
47
54
  path = movePrevious(path)
48
55
  if (path.length > 0) {
49
56
  currentNode = getCurrentNode(path)
50
57
  if (previousNode !== currentNode)
51
- moveTo(node, path, currentNode, idPrefix)
58
+ moveTo(element, path, currentNode, idPrefix)
52
59
  }
53
60
  }
54
61
  const select = () => {
55
- if (currentNode)
56
- node.dispatchEvent(
57
- new CustomEvent('select', {
58
- detail: { path: indicesFromPath(path), node: currentNode }
59
- })
60
- )
62
+ if (currentNode) emit('select', element, indicesFromPath(path), currentNode)
61
63
  }
62
64
  const collapse = () => {
63
65
  if (currentNode) {
64
- const collapse =
65
- hasChildren(currentNode, path[path.length - 1].fields) &&
66
- currentNode[path[path.length - 1].fields.isOpen]
66
+ const collapse = isExpanded(currentNode, path[path.length - 1].fields)
67
67
  if (collapse) {
68
68
  currentNode[path[path.length - 1].fields.isOpen] = false
69
- node.dispatchEvent(
70
- new CustomEvent('collapse', {
71
- detail: { path: indicesFromPath(path), node: currentNode }
72
- })
73
- )
69
+ emit('collapse', element, indicesFromPath(path), currentNode)
74
70
  } else if (path.length > 0) {
75
71
  path = path.slice(0, -1)
76
72
  currentNode = getCurrentNode(path)
@@ -81,11 +77,7 @@ export function navigator(node, options) {
81
77
  const expand = () => {
82
78
  if (currentNode && hasChildren(currentNode, path[path.length - 1].fields)) {
83
79
  currentNode[path[path.length - 1].fields.isOpen] = true
84
- node.dispatchEvent(
85
- new CustomEvent('expand', {
86
- detail: { path: indicesFromPath(path), node: currentNode }
87
- })
88
- )
80
+ emit('expand', element, indicesFromPath(path), currentNode)
89
81
  }
90
82
  }
91
83
 
@@ -111,7 +103,7 @@ export function navigator(node, options) {
111
103
  }
112
104
 
113
105
  const handleClick = (event) => {
114
- let target = findParentWithDataPath(event.target)
106
+ let target = findParentWithDataPath(event.target, element)
115
107
  let indices = !target
116
108
  ? []
117
109
  : target.dataset.path
@@ -128,52 +120,72 @@ export function navigator(node, options) {
128
120
  const event = currentNode[path[path.length - 1].fields.isOpen]
129
121
  ? 'expand'
130
122
  : 'collapse'
131
- node.dispatchEvent(
132
- new CustomEvent(event, {
133
- detail: { path: indices, node: currentNode }
134
- })
135
- )
136
- }
137
- node.dispatchEvent(
138
- new CustomEvent('select', {
139
- detail: { path: indices, node: currentNode }
140
- })
141
- )
123
+ emit(event, element, indices, currentNode)
124
+ } else if (currentNode) emit('select', element, indices, currentNode)
142
125
  }
143
126
  }
144
127
 
145
- node.addEventListener('keydown', handleKeyDown)
146
- node.addEventListener('click', handleClick)
128
+ element.addEventListener('keydown', handleKeyDown)
129
+ element.addEventListener('click', handleClick)
147
130
 
148
131
  return {
149
132
  update,
150
133
  destroy() {
151
- node.removeEventListener('keydown', handleKeyDown)
152
- node.removeEventListener('click', handleClick)
134
+ element.removeEventListener('keydown', handleKeyDown)
135
+ element.removeEventListener('click', handleClick)
153
136
  }
154
137
  }
155
138
  }
156
139
 
157
- export function moveTo(node, path, currentNode, idPrefix) {
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) {
158
149
  const indices = indicesFromPath(path)
159
-
160
- let current = node.querySelector('#' + idPrefix + indices.join('-'))
150
+ let current = element.querySelector('#' + idPrefix + indices.join('-'))
161
151
  if (current) current.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
162
152
 
163
- node.dispatchEvent(
164
- new CustomEvent('move', {
165
- detail: { path: indices, node: currentNode }
166
- })
167
- )
153
+ emit('move', element, indices, currentNode)
168
154
  }
169
155
 
170
- 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) {
171
164
  if (element.hasAttribute('data-path')) return element
172
165
  let parent = element.parentNode
173
166
 
174
- while (parent && !parent.hasAttribute('data-path')) {
167
+ while (parent && parent !== root && !parent.hasAttribute('data-path')) {
175
168
  parent = parent.parentNode
176
169
  }
177
170
 
178
- 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
+ )
179
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
  */
@@ -9,15 +9,24 @@ export function themable(node) {
9
9
  let previous = {}
10
10
 
11
11
  theme.subscribe((data) => {
12
- if (data.name && data.name !== previous.name) {
13
- node.classList.remove(previous.name)
14
- node.classList.add(data.name)
15
- }
16
- if (data.mode && data.mode !== previous.mode) {
17
- node.classList.remove(previous.mode)
18
- node.classList.add(data.mode)
19
- }
12
+ switchClass(node, data.name, previous.name)
13
+ switchClass(node, data.mode, previous.mode)
20
14
 
21
15
  previous = data
22
16
  })
23
17
  }
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
+ */
27
+ function switchClass(node, current, previous) {
28
+ if (current && current !== previous) {
29
+ node.classList.remove(previous)
30
+ node.classList.add(current)
31
+ }
32
+ }
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
+ */