@rokkit/actions 1.0.0-next.100 → 1.0.0-next.101

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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2022 Jerry Thomas
3
+ Copyright (c) 2025 Jerry Thomas
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1 +1,57 @@
1
- # Core Components
1
+ # Actions
2
+
3
+ This package provides a set of actions that can be used to perform various tasks.
4
+
5
+ ## Keyboard
6
+
7
+ The keyboard action can be used to map keyboard events to actions. The following example shows how to map the `K` key combination to an action:
8
+
9
+ The default behavior is to listen to keyup events.
10
+
11
+ - [x] Configuration driven
12
+ - [x] Custom events can be defined
13
+ - [x] Supports mapping an array of keys to an event
14
+ - [x] Supports mapping a regex to an event
15
+ - [ ] Support key modifiers
16
+ - [ ] Support a combination of regex patterns and array of keys
17
+
18
+ Default configuration
19
+
20
+ - _add_: alphabet keys cause an `add` event
21
+ - _submit_: enter causes a `submit` event
22
+ - _cancel_: escape causes a `cancel` event
23
+ - _delete_: backspace or delete causes a `delete` event
24
+
25
+ ### Basic Usage
26
+
27
+ ```svelte
28
+ <script>
29
+ import { keyboard } from '@fumbl/actions'
30
+
31
+ function handleKey(event) {
32
+ console.log(`${event.detail} pressed`)
33
+ }
34
+ </script>
35
+
36
+ <div use:keyboard onadd={handleKey}></div>
37
+ ```
38
+
39
+ ### Custom Events
40
+
41
+ ```svelte
42
+ <script>
43
+ import { keyboard } from '@fumbl/actions'
44
+ function handleKey(event) {
45
+ console.log(`${event.detail} pressed`)
46
+ }
47
+
48
+ const config = {
49
+ add: ['a', 'b', 'c'],
50
+ submit: 'enter',
51
+ cancel: 'escape',
52
+ delete: ['backspace', 'delete']
53
+ }
54
+ </script>
55
+
56
+ <div use:keyboard={config} onadd={handleKey}></div>
57
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rokkit/actions",
3
- "version": "1.0.0-next.100",
3
+ "version": "1.0.0-next.101",
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",
@@ -11,20 +11,6 @@
11
11
  "publishConfig": {
12
12
  "access": "public"
13
13
  },
14
- "devDependencies": {
15
- "@sveltejs/vite-plugin-svelte": "^3.1.2",
16
- "@testing-library/svelte": "^5.2.1",
17
- "@types/ramda": "^0.30.2",
18
- "@vitest/coverage-v8": "^2.1.1",
19
- "@vitest/ui": "~2.1.1",
20
- "jsdom": "^25.0.0",
21
- "svelte": "^4.2.19",
22
- "typescript": "^5.6.2",
23
- "vite": "^5.4.6",
24
- "vitest": "~2.1.1",
25
- "shared-config": "1.0.0-next.100",
26
- "validators": "1.0.0-next.100"
27
- },
28
14
  "files": [
29
15
  "src/**/*.js",
30
16
  "src/**/*.svelte"
@@ -39,18 +25,13 @@
39
25
  }
40
26
  },
41
27
  "dependencies": {
42
- "ramda": "^0.30.1",
43
- "@rokkit/core": "1.0.0-next.100",
44
- "@rokkit/stores": "1.0.0-next.100"
28
+ "ramda": "^0.30.1"
29
+ },
30
+ "devDependencies": {
31
+ "@rokkit/helpers": "1.0.0-next.101"
45
32
  },
46
33
  "scripts": {
47
- "format": "prettier --write .",
48
- "lint": "eslint --fix .",
49
- "test:ci": "vitest run",
50
- "test:ui": "vitest --ui",
51
- "test": "vitest",
52
- "coverage": "vitest run --coverage",
53
- "latest": "pnpm upgrade --latest && pnpm test:ci",
54
- "release": "pnpm publish --access public"
34
+ "clean": "rm -rf dist",
35
+ "build": "pnpm clean && pnpm prepublishOnly"
55
36
  }
56
37
  }
package/src/index.js CHANGED
@@ -1,13 +1,3 @@
1
1
  // skipcq: JS-E1004 - Needed for exposing all types
2
- export * from './types'
3
- // skipcq: JS-E1004 - Needed for exposing collection of functions
4
- export * from './lib'
5
- export { fillable } from './fillable'
6
- export { pannable } from './pannable'
7
- export { navigable } from './navigable'
8
- export { navigator } from './navigator'
9
- export { dismissable } from './dismissable'
10
- export { themable } from './themeable'
11
- export { swipeable } from './swipeable'
12
- export { switchable } from './switchable'
13
- export { delegateKeyboardEvents } from './delegate'
2
+ export * from './types.js'
3
+ export { keyboard } from './keyboard.svelte.js'
@@ -0,0 +1,58 @@
1
+ import { on } from 'svelte/events'
2
+ import { getClosestAncestorWithAttribute, getEventForKey } from './utils.js'
3
+
4
+ /**
5
+ * Default key mappings
6
+ * @type {import('./types.js').KeyboardConfig}
7
+ */
8
+ const defaultKeyMappings = {
9
+ remove: ['Backspace', 'Delete'],
10
+ submit: ['Enter'],
11
+ add: /^[a-zA-Z]$/
12
+ }
13
+
14
+ /**
15
+ * Handle keyboard events
16
+ *
17
+ * @param {HTMLElement} root
18
+ * @param {import('./types.js').KeyboardConfig} options - Custom key mappings
19
+ */
20
+ export function keyboard(root, options = defaultKeyMappings) {
21
+ const keyMappings = options || defaultKeyMappings
22
+
23
+ /**
24
+ * Handle keyboard events
25
+ *
26
+ * @param {KeyboardEvent} event
27
+ */
28
+ const keyup = (event) => {
29
+ const { key } = event
30
+ const eventName = getEventForKey(keyMappings, key)
31
+
32
+ if (eventName) {
33
+ root.dispatchEvent(new CustomEvent(eventName, { detail: key }))
34
+ }
35
+ }
36
+
37
+ const click = (event) => {
38
+ const node = getClosestAncestorWithAttribute(event.target, 'data-key')
39
+
40
+ if (node) {
41
+ const key = node.getAttribute('data-key')
42
+ const eventName = getEventForKey(keyMappings, key)
43
+
44
+ if (eventName) {
45
+ root.dispatchEvent(new CustomEvent(eventName, { detail: key }))
46
+ }
47
+ }
48
+ }
49
+
50
+ $effect(() => {
51
+ const cleanupKeyupEvent = on(document, 'keyup', keyup)
52
+ const cleanupClickEvent = on(root, 'click', click)
53
+ return () => {
54
+ cleanupKeyupEvent()
55
+ cleanupClickEvent()
56
+ }
57
+ })
58
+ }
package/src/types.js CHANGED
@@ -1,132 +1,10 @@
1
1
  /**
2
- * @typedef SvelteActionReturn
3
- * @property {() => void} destroy
4
- * @property {() => void} [update]
2
+ * @typedef {Object} EventMapping
3
+ * @property {string} event - The event name
4
+ * @property {string[]} [keys] - The keys that trigger the event
5
+ * @property {RegExp} [pattern] - The pattern that triggers the event
5
6
  */
6
7
 
7
8
  /**
8
- * @typedef FillableData
9
- * @property {string} value
10
- * @property {integer} actualIndex
11
- * @property {integer} expectedIndex
9
+ * @typedef {Object<string, (string[]|RegExp) >} KeyboardConfig
12
10
  */
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
- */
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
- */
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
- */
119
-
120
- /**
121
- * @typedef {Object} PushDownOptions
122
- * @property {string} selector - The CSS selector for the child element to which keyboard events will be forwarded.
123
- * @property {Array<string>} [events=['keydown', 'keyup', 'keypress']] - The keyboard events to forward.
124
- */
125
-
126
- /**
127
- * @typedef {Object} Bounds
128
- * @property {number} lower
129
- * @property {number} upper
130
- */
131
-
132
- export default {}
package/src/utils.js CHANGED
@@ -1,24 +1,28 @@
1
- export function handleAction(actions, event) {
2
- if (event.key in actions) {
3
- event.preventDefault()
4
- event.stopPropagation()
5
- actions[event.key]()
6
- }
1
+ /**
2
+ * Finds the closest ancestor of the given element that has the given attribute.
3
+ *
4
+ * @param {HTMLElement} element
5
+ * @param {string} attribute
6
+ * @returns {HTMLElement|null}
7
+ */
8
+ export function getClosestAncestorWithAttribute(element, attribute) {
9
+ if (!element) return null
10
+ if (element.getAttribute(attribute)) return element
11
+ return getClosestAncestorWithAttribute(element.parentElement, attribute)
7
12
  }
8
13
 
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
- }
14
+ import * as R from 'ramda'
15
+
16
+ /**
17
+ * Get the event name for a given key.
18
+ * @param {import('./types.js').KeyboardConfig} keyMapping
19
+ * @param {string} key - The key to match.
20
+ * @returns {string|null} - The event name or null if no match is found.
21
+ */
22
+ export const getEventForKey = (keyMapping, key) => {
23
+ const matchEvent = ([eventName, keys]) =>
24
+ (Array.isArray(keys) && keys.includes(key)) || (keys instanceof RegExp && keys.test(key))
25
+
26
+ const event = R.find(matchEvent, R.toPairs(keyMapping))
27
+ return event ? event[0] : null
24
28
  }
package/src/delegate.js DELETED
@@ -1,34 +0,0 @@
1
- import { EventManager } from './lib'
2
- /**
3
- * Svelte action function for forwarding keyboard events from a parent element to a child.
4
- * The child is selected using a CSS selector passed in the options object.
5
- * Optionally, you can specify which keyboard events you want to forward: "keydown", "keyup", and/or "keypress".
6
- * By default, all three events are forwarded.
7
- * The action returns an object with a destroy method.
8
- * The destroy method removes all event listeners from the parent.
9
- *
10
- * @param {HTMLElement} element - The parent element from which keyboard events will be forwarded.
11
- * @param {import('./types').PushDownOptions} options - The options object.
12
- * @returns {{destroy: Function}}
13
- */
14
- export function delegateKeyboardEvents(
15
- element,
16
- { selector, events = ['keydown', 'keyup', 'keypress'] }
17
- ) {
18
- const child = element.querySelector(selector)
19
- const handlers = {}
20
- const manager = EventManager(element)
21
-
22
- function forwardEvent(event) {
23
- child.dispatchEvent(new KeyboardEvent(event.type, event))
24
- }
25
-
26
- if (child) {
27
- events.forEach((event) => (handlers[event] = forwardEvent))
28
- manager.update(handlers)
29
- }
30
-
31
- return {
32
- destroy: () => manager.reset()
33
- }
34
- }
@@ -1,33 +0,0 @@
1
- const KEYCODE_ESC = 27
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
- */
10
- export function dismissable(node) {
11
- const handleClick = (event) => {
12
- if (node && !node.contains(event.target) && !event.defaultPrevented) {
13
- node.dispatchEvent(new CustomEvent('dismiss', node))
14
- }
15
- }
16
- const keyup = (event) => {
17
- if (event.keyCode === KEYCODE_ESC || event.key === 'Escape') {
18
- event.stopPropagation()
19
-
20
- node.dispatchEvent(new CustomEvent('dismiss', node))
21
- }
22
- }
23
-
24
- document.addEventListener('click', handleClick, true)
25
- document.addEventListener('keyup', keyup, true)
26
-
27
- return {
28
- destroy() {
29
- document.removeEventListener('click', handleClick, true)
30
- document.removeEventListener('keyup', keyup, true)
31
- }
32
- }
33
- }
package/src/fillable.js DELETED
@@ -1,106 +0,0 @@
1
- /**
2
- * Action for filling a <del>?</del> element in html block.
3
- *
4
- * @param {HTMLElement} node
5
- * @param {import('./types').FillOptions} options
6
- * @returns
7
- */
8
- export function fillable(node, { options, current, check }) {
9
- const data = { options, current, check }
10
- const blanks = node.getElementsByTagName('del')
11
-
12
- function click(event) {
13
- if (event.target.innerHTML !== '?') {
14
- clear(event, node)
15
- }
16
- }
17
-
18
- initialize(blanks, click)
19
-
20
- return {
21
- update(input) {
22
- data.options = input.options
23
- data.current = input.current
24
- data.check = check
25
-
26
- fill(blanks, data.options, data.current)
27
- if (data.check) validate(blanks, data)
28
- },
29
- destroy() {
30
- Object.keys(blanks).forEach((ref) => {
31
- blanks[ref].removeEventListener('click', click)
32
- })
33
- }
34
- }
35
- }
36
-
37
- /**
38
- * Initialize empty fillable element style and add listener for click
39
- *
40
- * @param {HTMLCollection} blanks
41
- * @param {EventListener} click
42
- */
43
- function initialize(blanks, click) {
44
- Object.keys(blanks).forEach((ref) => {
45
- blanks[ref].addEventListener('click', click)
46
- blanks[ref].classList.add('empty')
47
- blanks[ref].name = `fill-${ref}`
48
- blanks[ref]['data-index'] = ref
49
- })
50
- }
51
-
52
- /**
53
- * Fill current blank with provided option
54
- *
55
- * @param {HTMLCollection} blanks
56
- * @param {Array<import('./types.js').FillableData>} options
57
- * @param {*} current
58
- */
59
- function fill(blanks, options, current) {
60
- if (current > -1 && current < Object.keys(blanks).length) {
61
- const index = options.findIndex(({ actualIndex }) => actualIndex === current)
62
- if (index > -1) {
63
- blanks[current].innerHTML = options[index].value
64
- blanks[current].classList.remove('empty')
65
- blanks[current].classList.add('filled')
66
- }
67
- }
68
- }
69
-
70
- /**
71
- * Clear all fillable elements
72
- *
73
- * @param {EventListener} event
74
- * @param {HTMLElement} node
75
- */
76
- function clear(event, node) {
77
- event.target.innerHTML = '?'
78
- event.target.classList.remove('filled')
79
- event.target.classList.remove('pass')
80
- event.target.classList.remove('fail')
81
- event.target.classList.add('empty')
82
- node.dispatchEvent(
83
- new CustomEvent('remove', {
84
- detail: {
85
- index: event.target.name.split('-')[1],
86
- value: event.target['data-index']
87
- }
88
- })
89
- )
90
- }
91
-
92
- /**
93
- * Validate the filled values
94
- *
95
- * @param {HTMLCollection} blanks
96
- * @param {import('./types').FillOptions} data
97
- */
98
- function validate(blanks, data) {
99
- Object.keys(blanks).forEach((_, ref) => {
100
- const index = data.options.findIndex(({ actualIndex }) => actualIndex === ref)
101
- if (index > -1)
102
- blanks[ref].classList.add(
103
- data.options[index].expectedIndex === data.options[index].actualIndex ? 'pass' : 'fail'
104
- )
105
- })
106
- }