@tanstack/alpine-hotkeys 0.4.0 → 0.5.0
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": "@tanstack/alpine-hotkeys",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Alpine adapter for TanStack Hotkeys",
|
|
5
5
|
"author": "Tanner Linsley",
|
|
6
6
|
"license": "MIT",
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
"keywords": [
|
|
18
18
|
"alpine",
|
|
19
19
|
"tanstack",
|
|
20
|
-
"hotkeys"
|
|
20
|
+
"hotkeys",
|
|
21
|
+
"tanstack-intent"
|
|
21
22
|
],
|
|
22
23
|
"type": "module",
|
|
23
24
|
"types": "./dist/index.d.ts",
|
|
@@ -30,10 +31,11 @@
|
|
|
30
31
|
"node": ">=20"
|
|
31
32
|
},
|
|
32
33
|
"files": [
|
|
33
|
-
"dist"
|
|
34
|
+
"dist",
|
|
35
|
+
"skills"
|
|
34
36
|
],
|
|
35
37
|
"dependencies": {
|
|
36
|
-
"@tanstack/hotkeys": "0.
|
|
38
|
+
"@tanstack/hotkeys": "0.17.0",
|
|
37
39
|
"@tanstack/store": "^0.11.2"
|
|
38
40
|
},
|
|
39
41
|
"devDependencies": {
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: alpine-hotkeys
|
|
3
|
+
description: >-
|
|
4
|
+
Use when adding or fixing keyboard shortcuts in an Alpine.js app with
|
|
5
|
+
@tanstack/alpine-hotkeys: createHotkeysScope owners, the $hotkeys magic and
|
|
6
|
+
hotkeysPlugin, getter options, element targets with $refs, scopes activated
|
|
7
|
+
from the hotkey manager, app hotkeys, recorders, and held keys, or replacing
|
|
8
|
+
@keydown.window handlers.
|
|
9
|
+
metadata:
|
|
10
|
+
purpose: >-
|
|
11
|
+
Alpine bindings for TanStack Hotkeys: per-component owners, the $hotkeys
|
|
12
|
+
magic, getter options, manager-activated scopes, and the Alpine-specific
|
|
13
|
+
mistakes to avoid.
|
|
14
|
+
type: framework
|
|
15
|
+
framework: alpine
|
|
16
|
+
library: '@tanstack/alpine-hotkeys'
|
|
17
|
+
library_version: '0.4.0'
|
|
18
|
+
sources:
|
|
19
|
+
- src/createHotkeysScope.ts
|
|
20
|
+
- src/hotkeysPlugin.ts
|
|
21
|
+
- src/scope.ts
|
|
22
|
+
- src/createHotkey.ts
|
|
23
|
+
- src/createAppHotkeys.ts
|
|
24
|
+
- TanStack/hotkeys:docs/framework/alpine/quick-start.md
|
|
25
|
+
- TanStack/hotkeys:docs/framework/alpine/guides/app-hotkeys.md
|
|
26
|
+
requires:
|
|
27
|
+
- '@tanstack/hotkeys#hotkeys-core'
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
# TanStack Hotkeys in Alpine
|
|
31
|
+
|
|
32
|
+
Before starting, load the `@tanstack/hotkeys#hotkeys-core` skill (`npx @tanstack/intent@latest load @tanstack/hotkeys#hotkeys-core`).
|
|
33
|
+
|
|
34
|
+
```ts no-check
|
|
35
|
+
import Alpine from 'alpinejs'
|
|
36
|
+
import { createHotkeysScope } from '@tanstack/alpine-hotkeys'
|
|
37
|
+
|
|
38
|
+
Alpine.data('panel', () => ({
|
|
39
|
+
dirty: false,
|
|
40
|
+
hotkeys: createHotkeysScope(),
|
|
41
|
+
init() {
|
|
42
|
+
this.hotkeys.createHotkey(
|
|
43
|
+
'Mod+S',
|
|
44
|
+
() => save(),
|
|
45
|
+
() => ({ enabled: this.dirty }),
|
|
46
|
+
)
|
|
47
|
+
this.hotkeys.createHotkey(
|
|
48
|
+
'Escape',
|
|
49
|
+
() => closePanel(),
|
|
50
|
+
() => ({ target: this.$refs.panel }),
|
|
51
|
+
)
|
|
52
|
+
},
|
|
53
|
+
destroy() {
|
|
54
|
+
this.hotkeys.destroy()
|
|
55
|
+
},
|
|
56
|
+
}))
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
For markup-only components, install `Alpine.plugin(hotkeysPlugin)` before `Alpine.start()` and use the `$hotkeys` magic, which is destroyed with its element: `x-init="$hotkeys.createHotkey('Mod+S', () => save())"`.
|
|
60
|
+
|
|
61
|
+
## API
|
|
62
|
+
|
|
63
|
+
| API | What it's for |
|
|
64
|
+
| --------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
|
|
65
|
+
| `Alpine.plugin(hotkeysPlugin)` | Install the `$hotkeys` magic, an owner that's destroyed with its element. |
|
|
66
|
+
| `createHotkeysScope(defaults?)` | Create an owner for one `Alpine.data` component, with optional default options. |
|
|
67
|
+
| `provideAppHotkeys(appHotkeys, options?)` | Declare app hotkeys and load the user's custom hotkeys once at startup, since Alpine has no provider. |
|
|
68
|
+
| `getHotkeyManager().activateScope(scope, options?)` | Turn on a scope, such as an exclusive one for a dialog; it returns the function that turns the scope off. |
|
|
69
|
+
|
|
70
|
+
Each owner has these methods. Read their state through `.value`, or `.hotkeys` for registrations.
|
|
71
|
+
|
|
72
|
+
| Method | What it's for |
|
|
73
|
+
| -------------------------------------------- | -------------------------------------------------------------------------------------------------- |
|
|
74
|
+
| `createHotkey(hotkey, callback, options?)` | Register one chord, sequence, or app hotkey until the owner is destroyed; options can be a getter. |
|
|
75
|
+
| `createHotkeys(definitions, commonOptions?)` | Register several hotkeys, or a getter of a list built from data, in one call. |
|
|
76
|
+
| `createHotkeyRecorder(options)` | Record a chord or sequence the user presses, for a settings screen. |
|
|
77
|
+
| `createAppHotkeys()` | Get the declared app hotkeys with the user's custom hotkeys applied. |
|
|
78
|
+
| `createAppHotkeyList()` | List every app hotkey with its status, for a cheat sheet or settings screen. |
|
|
79
|
+
| `createCustomHotkeys()` | Read, change, and reset the user's custom hotkeys, and check a new one for conflicts. |
|
|
80
|
+
| `createHotkeyRegistrations()` | List every registered hotkey with its options and `meta`. |
|
|
81
|
+
| `createHotkeyHint(hotkey, options?)` | Get whether held modifiers should reveal a shortcut's hint. |
|
|
82
|
+
| `createKeyHold(key)` | Get whether one key is held. |
|
|
83
|
+
| `createHeldKeys()` | Get every key that's held right now. |
|
|
84
|
+
| `createHeldKeyCodes()` | Map each held key to its physical `event.code`. |
|
|
85
|
+
| `destroy()` | Release every hotkey, subscription, and recorder the owner created. |
|
|
86
|
+
|
|
87
|
+
The package also re-exports `@tanstack/hotkeys`, such as `formatForDisplay`, `matchesKeyboardEvent`, and `defineAppHotkeys`.
|
|
88
|
+
|
|
89
|
+
## Pitfalls
|
|
90
|
+
|
|
91
|
+
- **Destroy each owner.** Owners aren't tied to Alpine's lifecycle, so without `this.hotkeys.destroy()` in `destroy()`, a removed component's hotkeys keep firing. Create one owner per component instance, inside the `Alpine.data` factory.
|
|
92
|
+
- **`createHotkeysScope` isn't a hotkey scope.** It only owns registrations and sets defaults. For a dialog, activate a real scope in `init()` with `release = getHotkeyManager().activateScope(name, { exclusive: true })`, pass `{ scope: name }` to the dialog's hotkeys, and call `release()` in `destroy()`. Give each open dialog its own name.
|
|
93
|
+
- **Pass options as a getter.** `{ enabled: this.dirty }` is read once. Use `() => ({ enabled: this.dirty })`.
|
|
94
|
+
- **Use either `$hotkeys` or your own owner for a hotkey, not both.** Otherwise it registers twice.
|
|
95
|
+
- With server rendering, load the adapter only in the browser.
|
|
96
|
+
|
|
97
|
+
When declaring app hotkeys, building a cheat sheet, or letting users rebind shortcuts, load `@tanstack/hotkeys#app-hotkeys` and read [app hotkeys in Alpine](references/app-hotkeys.md).
|
|
98
|
+
|
|
99
|
+
## API discovery and docs
|
|
100
|
+
|
|
101
|
+
Signatures: `node_modules/@tanstack/alpine-hotkeys/dist/types.d.ts`. Guides: `https://tanstack.com/hotkeys/latest/docs/framework/alpine/guides/<page>.md`, such as `hotkeys` or `app-hotkeys`.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# App hotkeys in Alpine
|
|
2
|
+
|
|
3
|
+
The `@tanstack/hotkeys#app-hotkeys` skill owns the rules. This is the Alpine wiring.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
// app-hotkeys.ts
|
|
7
|
+
import { defineAppHotkeys } from '@tanstack/alpine-hotkeys'
|
|
8
|
+
|
|
9
|
+
export const appHotkeys = defineAppHotkeys({
|
|
10
|
+
'doc.save': { hotkey: 'Mod+S', meta: { name: 'Save' } },
|
|
11
|
+
'help.shortcuts': { hotkey: 'Mod+/', meta: { name: 'Keyboard shortcuts' } },
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
declare module '@tanstack/alpine-hotkeys' {
|
|
15
|
+
interface Register {
|
|
16
|
+
appHotkeys: typeof appHotkeys
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
// main.ts, before Alpine.start()
|
|
23
|
+
import { provideAppHotkeys } from '@tanstack/alpine-hotkeys'
|
|
24
|
+
import { appHotkeys } from './app-hotkeys'
|
|
25
|
+
|
|
26
|
+
provideAppHotkeys(appHotkeys, {
|
|
27
|
+
customHotkeys: JSON.parse(localStorage.getItem('hotkeys') ?? '{}'),
|
|
28
|
+
onCustomHotkeysChange: (custom) =>
|
|
29
|
+
localStorage.setItem('hotkeys', JSON.stringify(custom)),
|
|
30
|
+
})
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The app hotkey methods throw until `provideAppHotkeys` has run. When saved shortcuts come from a server, await them first. Register inside a getter so registrations follow rebinding: `this.hotkeys.createHotkey(() => appHotkeys.value['doc.save'], save)`, with `const appHotkeys = this.hotkeys.createAppHotkeys()`. Build a cheat sheet from `createAppHotkeyList().value`.
|
|
34
|
+
|
|
35
|
+
A settings row records with the owner's `createHotkeyRecorder`:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import Alpine from 'alpinejs'
|
|
39
|
+
import { createHotkeysScope, formatForDisplay } from '@tanstack/alpine-hotkeys'
|
|
40
|
+
import type { AppHotkeyId } from '@tanstack/alpine-hotkeys'
|
|
41
|
+
|
|
42
|
+
Alpine.data('shortcutRow', (id: AppHotkeyId) => {
|
|
43
|
+
const hotkeys = createHotkeysScope()
|
|
44
|
+
const appHotkeys = hotkeys.createAppHotkeys()
|
|
45
|
+
const custom = hotkeys.createCustomHotkeys()
|
|
46
|
+
const recorder = hotkeys.createHotkeyRecorder({
|
|
47
|
+
maxSteps: Infinity,
|
|
48
|
+
validate: (hotkey) => {
|
|
49
|
+
const [conflict] = custom.findConflicts(id, hotkey)
|
|
50
|
+
return conflict ? `Used by ${conflict.meta?.name ?? conflict.id}` : true
|
|
51
|
+
},
|
|
52
|
+
onRecord: (hotkey) => custom.setCustomHotkey(id, hotkey),
|
|
53
|
+
onClear: () => custom.setCustomHotkey(id, null),
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
recorder,
|
|
58
|
+
get label() {
|
|
59
|
+
const { meta, hotkey } = appHotkeys.value[id]
|
|
60
|
+
return `${meta?.name}: ${hotkey ? formatForDisplay(hotkey) : 'Unassigned'}`
|
|
61
|
+
},
|
|
62
|
+
reset: () => custom.resetCustomHotkey(id),
|
|
63
|
+
destroy: () => hotkeys.destroy(),
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```html
|
|
69
|
+
<div x-data="shortcutRow('doc.save')">
|
|
70
|
+
<span x-text="label"></span>
|
|
71
|
+
<button @click="recorder.startRecording()">Change</button>
|
|
72
|
+
<button @click="reset()">Reset</button>
|
|
73
|
+
</div>
|
|
74
|
+
```
|