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

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.38",
3
+ "version": "1.0.0-next.39",
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",
@@ -23,7 +23,7 @@
23
23
  "validators": "latest",
24
24
  "vite": "^4.4.7",
25
25
  "vitest": "~0.33.0",
26
- "shared-config": "1.0.0-next.38"
26
+ "shared-config": "1.0.0-next.39"
27
27
  },
28
28
  "files": [
29
29
  "src/**/*.js",
@@ -1,39 +1,23 @@
1
1
  import { compact } from '@rokkit/core'
2
2
 
3
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} [" "]
4
+ * Emits a custom event with the given data.
5
+ *
6
+ * @param {HTMLElement} element
7
+ * @param {string} event
8
+ * @param {*} data
9
+ * @returns {void}
29
10
  */
11
+ export function emit(element, event, data) {
12
+ element.dispatchEvent(new CustomEvent(event, { detail: data }))
13
+ }
30
14
 
31
15
  /**
32
16
  * Maps keyboard events to actions based on the given handlers and options.
33
17
  *
34
- * @param {ActionHandlers} handlers
35
- * @param {NavigationOptions} options
36
- * @returns {KeyboardActions}
18
+ * @param {import('../types').ActionHandlers} handlers
19
+ * @param {import('../types').NavigationOptions} options
20
+ * @returns {import('../types').KeyboardActions}
37
21
  */
38
22
  export function mapKeyboardEventsToActions(handlers, options) {
39
23
  const { next, previous, select, escape } = handlers
@@ -56,8 +40,50 @@ export function mapKeyboardEventsToActions(handlers, options) {
56
40
  })
57
41
  }
58
42
 
43
+ /**
44
+ * Finds the closest ancestor of the given element that has the given attribute.
45
+ *
46
+ * @param {HTMLElement} element
47
+ * @param {string} attribute
48
+ * @returns {HTMLElement|null}
49
+ */
59
50
  export function getClosestAncestorWithAttribute(element, attribute) {
60
51
  if (!element) return null
61
52
  if (element.getAttribute(attribute)) return element
62
53
  return getClosestAncestorWithAttribute(element.parentElement, attribute)
63
54
  }
55
+
56
+ /**
57
+ * Sets up event handlers based on the given options.
58
+ * Returns whether or not the event handlers are listening.
59
+ *
60
+ * @param {HTMLElement} element
61
+ * @param {import('../types').EventHandlers} listeners
62
+ * @param {import('../types').TraversableOptions} options
63
+ * @returns {void}
64
+ */
65
+ export function setupListeners(element, listeners, options) {
66
+ const { enabled } = { enabled: true, ...options }
67
+
68
+ if (enabled) {
69
+ Object.entries(listeners).forEach(([event, listener]) =>
70
+ element.addEventListener(event, listener)
71
+ )
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Removes event handlers based on the given options.
77
+ * Returns whether or not the event handlers are listening.
78
+ *
79
+ * @param {HTMLElement} element
80
+ * @param {import('../types').EventHandlers} listeners
81
+ * @returns {void}
82
+ */
83
+ export function removeListeners(element, listeners) {
84
+ if (listeners) {
85
+ Object.entries(listeners).forEach(([event, listener]) => {
86
+ element.removeEventListener(event, listener)
87
+ })
88
+ }
89
+ }
package/src/pannable.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { removeListeners, setupListeners } from './lib'
2
+
1
3
  /**
2
4
  * Handle drag and move events
3
5
  *
@@ -7,6 +9,18 @@
7
9
  export function pannable(node) {
8
10
  let x
9
11
  let y
12
+ let listeners = {
13
+ primary: {
14
+ mousedown: start,
15
+ touchstart: start
16
+ },
17
+ secondary: {
18
+ mousemove: move,
19
+ mouseup: stop,
20
+ touchmove: move,
21
+ touchend: stop
22
+ }
23
+ }
10
24
 
11
25
  function track(event, name, delta = {}) {
12
26
  x = event.clientX || event.touches[0].clientX
@@ -20,37 +34,26 @@ export function pannable(node) {
20
34
  )
21
35
  }
22
36
 
23
- function handleMousedown(event) {
37
+ function start(event) {
24
38
  track(event, 'panstart')
25
- window.addEventListener('mousemove', handleMousemove)
26
- window.addEventListener('mouseup', handleMouseup)
27
- window.addEventListener('touchmove', handleMousemove, { passive: false })
28
- window.addEventListener('touchend', handleMouseup)
39
+ setupListeners(window, listeners.secondary)
29
40
  }
30
41
 
31
- function handleMousemove(event) {
42
+ function move(event) {
32
43
  const dx = (event.clientX || event.touches[0].clientX) - x
33
44
  const dy = (event.clientY || event.touches[0].clientY) - y
34
45
 
35
46
  track(event, 'panmove', { dx, dy })
36
47
  }
37
48
 
38
- function handleMouseup(event) {
49
+ function stop(event) {
39
50
  track(event, 'panend')
40
-
41
- window.removeEventListener('mousemove', handleMousemove)
42
- window.removeEventListener('mouseup', handleMouseup)
43
- window.removeEventListener('touchmove', handleMousemove)
44
- window.removeEventListener('touchend', handleMouseup)
51
+ removeListeners(window, listeners.secondary)
45
52
  }
46
53
 
47
- node.addEventListener('mousedown', handleMousedown)
48
- node.addEventListener('touchstart', handleMousedown, { passive: false })
54
+ setupListeners(node, listeners.primary)
49
55
 
50
56
  return {
51
- destroy() {
52
- node.removeEventListener('mousedown', handleMousedown)
53
- node.removeEventListener('touchstart', handleMousedown)
54
- }
57
+ destroy: () => removeListeners(node, listeners.primary)
55
58
  }
56
59
  }
package/src/swipeable.js CHANGED
@@ -6,87 +6,82 @@
6
6
  * @returns {import('./types').SvelteActionReturn}
7
7
  */
8
8
 
9
- export function swipeable(
10
- node,
11
- {
12
- horizontal = true,
13
- vertical = false,
14
- threshold = 100,
15
- enabled = true,
16
- minSpeed = 300
17
- } = {}
18
- ) {
19
- let listening = false
20
- let startX
21
- let startY
22
- let startTime
9
+ import { removeListeners, setupListeners } from './lib'
10
+ const defaultOptions = {
11
+ horizontal: true,
12
+ vertical: false,
13
+ threshold: 100,
14
+ enabled: true,
15
+ minSpeed: 300
16
+ }
17
+ export function swipeable(node, options = defaultOptions) {
18
+ let track = {}
19
+ let listeners = {}
23
20
 
24
- function touchStart(event) {
25
- const touch = event.touches ? event.touches[0] : event
26
- startX = touch.clientX
27
- startY = touch.clientY
28
- startTime = new Date().getTime()
21
+ const updateListeners = (options) => {
22
+ removeListeners(node, listeners)
23
+ listeners = getListeners(node, options, track)
24
+ setupListeners(node, listeners, options)
29
25
  }
30
26
 
31
- function touchEnd(event) {
32
- const touch = event.changedTouches ? event.changedTouches[0] : event
33
- const distX = touch.clientX - startX
34
- const distY = touch.clientY - startY
35
- const duration = (new Date().getTime() - startTime) / 1000
36
- const speed = Math.max(Math.abs(distX), Math.abs(distY)) / duration
37
-
38
- if (horizontal && speed > minSpeed) {
39
- if (Math.abs(distX) > Math.abs(distY) && Math.abs(distX) >= threshold) {
40
- if (distX > 0 && distX / duration > minSpeed) {
41
- node.dispatchEvent(new CustomEvent('swipeRight'))
42
- } else {
43
- node.dispatchEvent(new CustomEvent('swipeLeft'))
44
- }
45
- }
46
- }
27
+ options = { ...defaultOptions, ...options }
28
+ updateListeners(options)
47
29
 
48
- if (vertical && speed > minSpeed) {
49
- if (Math.abs(distY) > Math.abs(distX) && Math.abs(distY) >= threshold) {
50
- if (distY > 0) {
51
- node.dispatchEvent(new CustomEvent('swipeDown'))
52
- } else {
53
- node.dispatchEvent(new CustomEvent('swipeUp'))
54
- }
55
- }
30
+ return {
31
+ update: (data) => {
32
+ options = { ...options, ...data }
33
+ updateListeners(options)
34
+ },
35
+ destroy() {
36
+ removeListeners(node, listeners)
56
37
  }
57
38
  }
39
+ }
58
40
 
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) {
68
- node.removeEventListener('touchstart', touchStart)
69
- node.removeEventListener('touchend', touchEnd)
70
- node.removeEventListener('mousedown', touchStart)
71
- node.removeEventListener('mouseup', touchEnd)
72
- listening = false
73
- }
41
+ function getListeners(node, options, track) {
42
+ if (!options.enabled) return {}
43
+
44
+ let listeners = {
45
+ touchend: (e) => touchEnd(e, node, options, track),
46
+ touchstart: (e) => touchStart(e, track),
47
+ mousedown: (e) => touchStart(e, track),
48
+ mouseup: (e) => touchEnd(e, node, options, track)
74
49
  }
50
+ return listeners
51
+ }
75
52
 
76
- updateListeners(enabled)
53
+ function touchStart(event, track) {
54
+ const touch = event.touches ? event.touches[0] : event
55
+ track.startX = touch.clientX
56
+ track.startY = touch.clientY
57
+ track.startTime = new Date().getTime()
58
+ }
77
59
 
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
60
+ function touchEnd(event, node, options, track) {
61
+ const { horizontal, vertical, threshold, minSpeed } = options
62
+ const touch = event.changedTouches ? event.changedTouches[0] : event
63
+ const distX = touch.clientX - track.startX
64
+ const distY = touch.clientY - track.startY
65
+ const duration = (new Date().getTime() - track.startTime) / 1000
66
+ const speed = Math.max(Math.abs(distX), Math.abs(distY)) / duration
85
67
 
86
- updateListeners(enabled)
87
- },
88
- destroy() {
89
- updateListeners(false)
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
+ }
77
+
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
+ }
90
85
  }
91
86
  }
92
87
  }
@@ -1,6 +1,9 @@
1
1
  import {
2
2
  getClosestAncestorWithAttribute,
3
- mapKeyboardEventsToActions
3
+ mapKeyboardEventsToActions,
4
+ setupListeners,
5
+ removeListeners,
6
+ emit
4
7
  } from './lib'
5
8
 
6
9
  const defaultOptions = {
@@ -8,22 +11,48 @@ const defaultOptions = {
8
11
  nested: false,
9
12
  enabled: true
10
13
  }
14
+
15
+ /**
16
+ * An action that can be used to traverse a nested list of items using keyboard and mouse.
17
+ *
18
+ * @param {HTMLElement} element
19
+ * @param {import('./types').TraversableOptions} data
20
+ * @returns
21
+ */
11
22
  export function traversable(element, data) {
12
- let listening = false
13
23
  let options = {}
14
24
  let tracker = {}
15
- let handlers = {}
25
+ let handlers
26
+ let actions
27
+ let listeners
16
28
 
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)
29
+ const configure = (data) => {
30
+ options = { ...options, ...data }
31
+
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)
37
+ }
38
+
39
+ configure({ ...defaultOptions, ...data })
40
+
41
+ return {
42
+ update: configure,
43
+ destroy: () => removeListeners(element, listeners)
24
44
  }
45
+ }
25
46
 
26
- let listeners = {
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 {
27
56
  keydown: (event) => {
28
57
  const action = handlers[event.key]
29
58
  if (action) action(event)
@@ -38,40 +67,24 @@ export function traversable(element, data) {
38
67
  }
39
68
  }
40
69
  }
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
70
  }
58
71
 
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
- )
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)
70
86
  }
71
- return enabled
87
+ return actions
72
88
  }
73
89
 
74
- function emit(element, event, tracker) {
75
- element.dispatchEvent(new CustomEvent(event, { detail: tracker }))
76
- }
77
90
  // function handleValueChange(element, options) {}
package/src/types.js CHANGED
@@ -51,3 +51,61 @@
51
51
  * @property {number} threshold - Threshold for swipe
52
52
  * @property {number} minSpeed - Minimum speed for swipe
53
53
  */
54
+
55
+ /**
56
+ * @typedef TraversableOptions
57
+ * @property {boolean} horizontal - Traverse horizontally
58
+ * @property {boolean} nested - Traverse nested items
59
+ * @property {boolean} enabled - Enable traversal
60
+ * @property {string} value - Value to be used for traversal
61
+ * @property {Array<*>} items - An array containing the data set to traverse
62
+ * @property {Array<integer} [indices] - Indices of the items to be traversed
63
+ */
64
+
65
+ /**
66
+ * @typedef PositionTracker
67
+ * @property {integer} index
68
+ * @property {integer} previousIndex
69
+ */
70
+
71
+ /**
72
+ * @typedef EventHandlers
73
+ * @property {function} [keydown]
74
+ * @property {function} [keyup]
75
+ * @property {function} [click]
76
+ * @property {function} [touchstart]
77
+ * @property {function} [touchmove]
78
+ * @property {function} [touchend]
79
+ * @property {function} [touchcancel]
80
+ * @property {function} [mousedown]
81
+ * @property {function} [mouseup]
82
+ * @property {function} [mousemove]
83
+ */
84
+
85
+ /**
86
+ * @typedef {Object} ActionHandlers
87
+ * @property {Function} [next]
88
+ * @property {Function} [previous]
89
+ * @property {Function} [select]
90
+ * @property {Function} [escape]
91
+ * @property {Function} [collapse]
92
+ * @property {Function} [expand]
93
+ */
94
+
95
+ /**
96
+ * @typedef {Object} NavigationOptions
97
+ * @property {Boolean} [horizontal]
98
+ * @property {Boolean} [nested]
99
+ * @property {Boolean} [enabled]
100
+ */
101
+
102
+ /**
103
+ * @typedef {Object} KeyboardActions
104
+ * @property {Function} [ArrowDown]
105
+ * @property {Function} [ArrowUp]
106
+ * @property {Function} [ArrowRight]
107
+ * @property {Function} [ArrowLeft]
108
+ * @property {Function} [Enter]
109
+ * @property {Function} [Escape]
110
+ * @property {Function} [" "]
111
+ */