@rokkit/actions 1.0.0-next.39 → 1.0.0-next.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rokkit/actions",
3
- "version": "1.0.0-next.39",
3
+ "version": "1.0.0-next.43",
4
4
  "description": "Contains generic actions that can be used in various components.",
5
5
  "author": "Jerry Thomas <me@jerrythomas.name>",
6
6
  "license": "MIT",
@@ -15,6 +15,7 @@
15
15
  "devDependencies": {
16
16
  "@sveltejs/vite-plugin-svelte": "^2.4.3",
17
17
  "@testing-library/svelte": "^4.0.3",
18
+ "@types/ramda": "^0.29.3",
18
19
  "@vitest/coverage-v8": "^0.33.0",
19
20
  "@vitest/ui": "~0.33.0",
20
21
  "jsdom": "^22.1.0",
@@ -23,7 +24,7 @@
23
24
  "validators": "latest",
24
25
  "vite": "^4.4.7",
25
26
  "vitest": "~0.33.0",
26
- "shared-config": "1.0.0-next.39"
27
+ "shared-config": "1.0.0-next.43"
27
28
  },
28
29
  "files": [
29
30
  "src/**/*.js",
@@ -41,7 +42,8 @@
41
42
  },
42
43
  "dependencies": {
43
44
  "@rokkit/core": "latest",
44
- "@rokkit/stores": "latest"
45
+ "@rokkit/stores": "latest",
46
+ "ramda": "^0.29.0"
45
47
  },
46
48
  "scripts": {
47
49
  "format": "prettier --write .",
@@ -52,6 +54,6 @@
52
54
  "test": "vitest",
53
55
  "coverage": "vitest run --coverage",
54
56
  "latest": "pnpm upgrade --latest && pnpm test:ci",
55
- "release": "tsc && pnpm publish --access public"
57
+ "release": "pnpm publish --access public"
56
58
  }
57
59
  }
package/src/hierarchy.js CHANGED
@@ -46,20 +46,29 @@ export function moveNext(path, items, fields) {
46
46
  } else if (current.index < current.items.length - 1) {
47
47
  current.index++
48
48
  } else {
49
- let level = path.length - 2
50
- while (level >= 0) {
51
- const parent = path[level]
52
- if (parent.index < parent.items.length - 1) {
53
- parent.index++
54
- path = path.slice(0, level + 1)
55
- break
56
- }
57
- level--
58
- }
49
+ path = navigateToNextLevel(path)
59
50
  }
60
51
  return path
61
52
  }
62
53
 
54
+ /**
55
+ * Navigate to the next level
56
+ * @param {Array<import('./types').PathFragment>} path
57
+ * @returns {Array<import('./types').PathFragment>}
58
+ */
59
+ function navigateToNextLevel(path) {
60
+ let level = path.length - 2
61
+ while (level >= 0) {
62
+ const parent = path[level]
63
+ if (parent.index < parent.items.length - 1) {
64
+ parent.index++
65
+ path = path.slice(0, level + 1)
66
+ break
67
+ }
68
+ level--
69
+ }
70
+ return path
71
+ }
63
72
  /**
64
73
  * Navigate to the previous item
65
74
  *
@@ -0,0 +1,32 @@
1
+ /**
2
+ * EventManager class to manage event listeners on an element.
3
+ */
4
+ export function EventManager(element, handlers = {}) {
5
+ let listening = false
6
+
7
+ function activate() {
8
+ if (!listening) {
9
+ for (const event in handlers) {
10
+ element.addEventListener(event, handlers[event])
11
+ }
12
+ listening = true
13
+ }
14
+ }
15
+ function destroy() {
16
+ if (listening) {
17
+ for (const event in handlers) {
18
+ element.removeEventListener(event, handlers[event])
19
+ }
20
+ listening = false
21
+ }
22
+ }
23
+ function update(enabled, newHandlers = handlers) {
24
+ if (listening !== enabled || handlers !== newHandlers) {
25
+ destroy()
26
+ handlers = newHandlers
27
+ if (enabled) activate()
28
+ }
29
+ }
30
+
31
+ return { activate, destroy, update }
32
+ }
package/src/lib/index.js CHANGED
@@ -1 +1,2 @@
1
1
  export * from './internal'
2
+ export * from './event-manager'
@@ -1,4 +1,5 @@
1
1
  import { compact } from '@rokkit/core'
2
+ import { hasChildren, isExpanded } from '@rokkit/core'
2
3
 
3
4
  /**
4
5
  * Emits a custom event with the given data.
@@ -64,7 +65,6 @@ export function getClosestAncestorWithAttribute(element, attribute) {
64
65
  */
65
66
  export function setupListeners(element, listeners, options) {
66
67
  const { enabled } = { enabled: true, ...options }
67
-
68
68
  if (enabled) {
69
69
  Object.entries(listeners).forEach(([event, listener]) =>
70
70
  element.addEventListener(event, listener)
@@ -87,3 +87,27 @@ export function removeListeners(element, listeners) {
87
87
  })
88
88
  }
89
89
  }
90
+
91
+ /**
92
+ * Handles the click event.
93
+ * @param {HTMLElement} element - The root element.
94
+ * @param {CurrentItem} current - A reference to the current Item
95
+ * @returns {CurrentItem} The updated current item.
96
+ */
97
+ export function handleItemClick(element, current) {
98
+ const { item, fields, position } = current
99
+ const detail = { item, position }
100
+
101
+ if (hasChildren(item, fields)) {
102
+ if (isExpanded(item, fields)) {
103
+ item[fields.isOpen] = false
104
+ emit(element, 'collapse', detail)
105
+ } else {
106
+ item[fields.isOpen] = true
107
+ emit(element, 'expand', detail)
108
+ }
109
+ } else {
110
+ emit(element, 'select', detail)
111
+ }
112
+ return current
113
+ }
package/src/navigator.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  indicesFromPath,
8
8
  getCurrentNode
9
9
  } from './hierarchy'
10
-
10
+ import { mapKeyboardEventsToActions } from './lib'
11
11
  /**
12
12
  * Keyboard navigation for Lists and NestedLists. The data is either nested or not and is not
13
13
  * expected to switch from nested to simple list or vice-versa.
@@ -83,11 +83,15 @@ export function navigator(element, options) {
83
83
  update(options)
84
84
 
85
85
  const nested = isNested(items, fields)
86
- const actions = mapKeyboardEventsToActions(vertical, nested, handlers)
86
+ const actions = mapKeyboardEventsToActions(handlers, {
87
+ horizontal: !vertical,
88
+ nested
89
+ })
87
90
 
88
91
  const handleKeyDown = (event) => handleAction(actions, event)
89
92
 
90
93
  const handleClick = (event) => {
94
+ event.stopPropagation()
91
95
  let target = findParentWithDataPath(event.target, element)
92
96
  let indices = !target
93
97
  ? []
@@ -106,7 +110,8 @@ export function navigator(element, options) {
106
110
  ? 'expand'
107
111
  : 'collapse'
108
112
  emit(event, element, indices, currentNode)
109
- } else if (currentNode) emit('select', element, indices, currentNode)
113
+ } else if (currentNode !== null)
114
+ emit('select', element, indices, currentNode)
110
115
  }
111
116
  }
112
117
 
@@ -174,27 +179,3 @@ function emit(event, element, indices, node) {
174
179
  })
175
180
  )
176
181
  }
177
-
178
- function mapKeyboardEventsToActions(vertical, nested, handlers) {
179
- let actions = { Enter: handlers.select }
180
-
181
- if (vertical) {
182
- actions = {
183
- ...actions,
184
- ...{ ArrowDown: handlers.next, ArrowUp: handlers.previous },
185
- ...(nested
186
- ? { ArrowRight: handlers.expand, ArrowLeft: handlers.collapse }
187
- : {})
188
- }
189
- } else {
190
- actions = {
191
- ...actions,
192
- ...{ ArrowRight: handlers.next, ArrowLeft: handlers.previous },
193
- ...(nested
194
- ? { ArrowDown: handlers.expand, ArrowUp: handlers.collapse }
195
- : {})
196
- }
197
- }
198
-
199
- return actions
200
- }
package/src/swipeable.js CHANGED
@@ -1,12 +1,5 @@
1
- /**
2
- * A svelte action function that captures swipe actions and emits event for corresponding movements.
3
- *
4
- * @param {HTMLElement} node
5
- * @param {import(./types).SwipeableOptions} options
6
- * @returns {import('./types').SvelteActionReturn}
7
- */
8
-
9
1
  import { removeListeners, setupListeners } from './lib'
2
+
10
3
  const defaultOptions = {
11
4
  horizontal: true,
12
5
  vertical: false,
@@ -14,6 +7,14 @@ const defaultOptions = {
14
7
  enabled: true,
15
8
  minSpeed: 300
16
9
  }
10
+
11
+ /**
12
+ * A svelte action function that captures swipe actions and emits event for corresponding movements.
13
+ *
14
+ * @param {HTMLElement} node
15
+ * @param {import(./types).SwipeableOptions} options
16
+ * @returns {import('./types').SvelteActionReturn}
17
+ */
17
18
  export function swipeable(node, options = defaultOptions) {
18
19
  let track = {}
19
20
  let listeners = {}
@@ -38,6 +39,13 @@ export function swipeable(node, options = defaultOptions) {
38
39
  }
39
40
  }
40
41
 
42
+ /**
43
+ * Returns the listeners for the swipeable action.
44
+ * @param {HTMLElement} node - The node where the event is dispatched.
45
+ * @param {import(./types).SwipeableOptions} options - The options for the swipe.
46
+ * @param {import(./types).TouchTracker} track - The tracking object.
47
+ * @returns {import(./types).Listeners}
48
+ */
41
49
  function getListeners(node, options, track) {
42
50
  if (!options.enabled) return {}
43
51
 
@@ -50,6 +58,12 @@ function getListeners(node, options, track) {
50
58
  return listeners
51
59
  }
52
60
 
61
+ /**
62
+ * Handles the touch start event.
63
+ *
64
+ * @param {Event} event
65
+ * @param {import(./types).TouchTracker} track
66
+ */
53
67
  function touchStart(event, track) {
54
68
  const touch = event.touches ? event.touches[0] : event
55
69
  track.startX = touch.clientX
@@ -57,31 +71,38 @@ function touchStart(event, track) {
57
71
  track.startTime = new Date().getTime()
58
72
  }
59
73
 
74
+ /**
75
+ * Handles the touch end event.
76
+ *
77
+ * @param {Event} event - The touch event.
78
+ * @param {HTMLElement} node - The node where the event is dispatched.
79
+ * @param {import(./types).SwipeableOptions} options options - The options for the swipe.
80
+ * @param {import(./types).TouchTracker} track - The tracking object.
81
+ */
60
82
  function touchEnd(event, node, options, track) {
61
- const { horizontal, vertical, threshold, minSpeed } = options
62
83
  const touch = event.changedTouches ? event.changedTouches[0] : event
63
84
  const distX = touch.clientX - track.startX
64
85
  const distY = touch.clientY - track.startY
65
86
  const duration = (new Date().getTime() - track.startTime) / 1000
66
87
  const speed = Math.max(Math.abs(distX), Math.abs(distY)) / duration
67
88
 
68
- if (horizontal && speed > minSpeed) {
69
- if (Math.abs(distX) > Math.abs(distY) && Math.abs(distX) >= threshold) {
70
- if (distX > 0 && distX / duration > minSpeed) {
71
- node.dispatchEvent(new CustomEvent('swipeRight'))
72
- } else {
73
- node.dispatchEvent(new CustomEvent('swipeLeft'))
74
- }
75
- }
76
- }
89
+ if (speed <= options.minSpeed) return
77
90
 
78
- if (vertical && speed > minSpeed) {
79
- if (Math.abs(distY) > Math.abs(distX) && Math.abs(distY) >= threshold) {
80
- if (distY > 0) {
81
- node.dispatchEvent(new CustomEvent('swipeDown'))
82
- } else {
83
- node.dispatchEvent(new CustomEvent('swipeUp'))
84
- }
85
- }
91
+ const isHorizontalSwipe =
92
+ options.horizontal && Math.abs(distX) >= options.threshold
93
+ const isVerticalSwipe =
94
+ options.vertical && Math.abs(distY) >= options.threshold
95
+
96
+ if (!isHorizontalSwipe && !isVerticalSwipe) return
97
+
98
+ const swipeDirection = getSwipeDirection(distX, distY)
99
+ node.dispatchEvent(new CustomEvent(`swipe${swipeDirection}`))
100
+ }
101
+
102
+ function getSwipeDirection(distX, distY) {
103
+ if (Math.abs(distX) > Math.abs(distY)) {
104
+ return distX > 0 ? 'Right' : 'Left'
105
+ } else {
106
+ return distY > 0 ? 'Down' : 'Up'
86
107
  }
87
108
  }
@@ -1,9 +1,11 @@
1
+ import { mappedList, isNested } from '@rokkit/core'
2
+ import { pick } from 'ramda'
1
3
  import {
2
4
  getClosestAncestorWithAttribute,
3
5
  mapKeyboardEventsToActions,
4
- setupListeners,
5
- removeListeners,
6
- emit
6
+ emit,
7
+ handleItemClick,
8
+ EventManager
7
9
  } from './lib'
8
10
 
9
11
  const defaultOptions = {
@@ -19,72 +21,81 @@ const defaultOptions = {
19
21
  * @param {import('./types').TraversableOptions} data
20
22
  * @returns
21
23
  */
22
- export function traversable(element, data) {
23
- let options = {}
24
- let tracker = {}
25
- let handlers
26
- let actions
27
- let listeners
24
+ export function traversable(element, options) {
25
+ const content = mappedList(options.items, options.fields)
26
+ const manager = EventManager(element)
28
27
 
29
- const configure = (data) => {
30
- options = { ...options, ...data }
28
+ let current = { position: [], item: null }
29
+ let handlers = {}
31
30
 
32
- removeListeners(element, listeners)
33
- actions = getActions(element, tracker)
34
- handlers = mapKeyboardEventsToActions(actions, options)
35
- listeners = getListeners(handlers, actions, tracker)
36
- setupListeners(element, listeners, options)
31
+ const moveCursor = (direction) => {
32
+ const result = content[direction](current.position)
33
+ if (result) {
34
+ current = result
35
+
36
+ checkAndEmit('move')
37
+ }
37
38
  }
38
39
 
39
- configure({ ...defaultOptions, ...data })
40
+ const checkAndEmit = (event) => {
41
+ if (current && current.item) {
42
+ emit(element, event, pick(['item', 'position'], current))
43
+ }
44
+ }
40
45
 
41
- return {
42
- update: configure,
43
- destroy: () => removeListeners(element, listeners)
46
+ const actions = {
47
+ next: () => moveCursor('next'),
48
+ previous: () => moveCursor('previous'),
49
+ select: () => checkAndEmit('select'),
50
+ escape: () => checkAndEmit('escape'),
51
+ collapse: () => checkAndEmit('collapse'),
52
+ expand: () => checkAndEmit('expand')
44
53
  }
45
- }
46
54
 
47
- /**
48
- * Returns the listeners for the given handlers and actions.
49
- *
50
- * @param {import('./types').KeyboardActions} handlers
51
- * @param {import('./types').ActionHandlers} actions
52
- * @param {import('./types').PositionTracker} tracker
53
- */
54
- function getListeners(handlers, actions, tracker) {
55
- return {
55
+ const listeners = {
56
56
  keydown: (event) => {
57
- const action = handlers[event.key]
58
- if (action) action(event)
59
- },
60
- click: (event) => {
61
- const target = getClosestAncestorWithAttribute(event.target, 'data-index')
62
-
63
- if (target) {
64
- const index = parseInt(target.getAttribute('data-index'))
65
- tracker.index = index
66
- actions.select()
67
- }
57
+ if (event.key in handlers) handlers[event.key](event)
68
58
  }
59
+ // click: (event) => {
60
+ // const target = getClosestAncestorWithAttribute(event.target, 'data-path')
61
+ // if (target) {
62
+ // const position = target
63
+ // .getAttribute('data-path')
64
+ // .split(',')
65
+ // .map((i) => +i)
66
+ // current = content.findByPosition(position)
67
+ // current = handleItemClick(element, current)
68
+ // }
69
+ // }
69
70
  }
70
- }
71
71
 
72
- /**
73
- *
74
- * @param {HTMLElement} element
75
- * @param {import('./types').PositionTracker} tracker
76
- * @returns {import('./types').ActionHandlers}
77
- */
78
- function getActions(element, tracker) {
79
- const actions = {
80
- next: () => emit(element, 'move', tracker),
81
- previous: () => emit(element, 'move', tracker),
82
- select: () => emit(element, 'select', tracker),
83
- escape: () => emit(element, 'escape', tracker),
84
- collapse: () => emit(element, 'collapse', tracker),
85
- expand: () => emit(element, 'expand', tracker)
72
+ const update = (data) => {
73
+ options = { ...defaultOptions, ...options, ...data }
74
+ options.nested = isNested(options.items, options.fields)
75
+ content.update(options.items, options.fields)
76
+ handlers = mapKeyboardEventsToActions(actions, options)
77
+ manager.update(options.enabled, listeners)
78
+ // current = handleValueChange(element, data, content, current)
79
+ }
80
+
81
+ update(options)
82
+
83
+ return {
84
+ update,
85
+ destroy: () => manager.destroy()
86
86
  }
87
- return actions
88
87
  }
89
88
 
90
- // function handleValueChange(element, options) {}
89
+ // const handleValueChange = (element, data, content, current) => {
90
+ // if (data.value !== null && data.value !== current?.value) {
91
+ // current = content.findByValue(data.value)
92
+ // if (current) scrollIntoView(element, current.position)
93
+ // }
94
+ // return current
95
+ // }
96
+
97
+ // function scrollIntoView(element, position) {
98
+ // if (!Array.isArray(position) || position.length == 0) return
99
+ // const node = element.querySelector(`[data-index="${position.join(',')}"]`)
100
+ // if (node) node.scrollIntoView()
101
+ // }
package/src/types.js CHANGED
@@ -109,3 +109,10 @@
109
109
  * @property {Function} [Escape]
110
110
  * @property {Function} [" "]
111
111
  */
112
+
113
+ /**
114
+ * @typedef {Object} TouchTracker
115
+ * @property {number} startX - The start X position of the touch.
116
+ * @property {number} startY - The start Y position of the touch.
117
+ * @property {number} startTime - The start time of the touch.
118
+ */