@joknoll/svelte-attach-key 0.1.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/README.md +102 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.js +1 -0
- package/package.json +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# @joknoll/svelte-attach-key
|
|
2
|
+
|
|
3
|
+
Svelte 5 attachments for wiring keyboard shortcuts to DOM elements.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
npm install @joknoll/svelte-attach-key
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```svelte
|
|
14
|
+
<script lang="ts">
|
|
15
|
+
import { hotkey, pressed, formatHint } from "@joknoll/svelte-attach-key";
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<button
|
|
19
|
+
{@attach hotkey("mod+k")}
|
|
20
|
+
onclick={() => console.log("opened")}
|
|
21
|
+
>
|
|
22
|
+
Open command menu
|
|
23
|
+
</button>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`hotkey("mod+k")` sets `aria-keyshortcuts="mod+k"` on the element automatically. `pressed()` reads it back to know which key to watch.
|
|
27
|
+
|
|
28
|
+
Pass alternative shortcuts as an array — either key triggers the element:
|
|
29
|
+
|
|
30
|
+
```svelte
|
|
31
|
+
<button
|
|
32
|
+
title={formatHint(["j", "arrowdown"])}
|
|
33
|
+
{@attach hotkey(["j", "arrowdown"])}
|
|
34
|
+
>
|
|
35
|
+
Move down
|
|
36
|
+
</button>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Attach a callback instead of relying on `click`:
|
|
40
|
+
|
|
41
|
+
```svelte
|
|
42
|
+
<button {@attach hotkey("mod+s", (e) => save())}>Save</button>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Conditional attachment:
|
|
46
|
+
|
|
47
|
+
```svelte
|
|
48
|
+
<button {@attach enabled && hotkey("k")}>Press K</button>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## API
|
|
52
|
+
|
|
53
|
+
### `hotkey(keys, onTrigger?, options?)`
|
|
54
|
+
|
|
55
|
+
Triggers `onTrigger` (or `node.click()`) when a matching key is pressed. Sets `aria-keyshortcuts` on the element and restores the prior value on cleanup.
|
|
56
|
+
|
|
57
|
+
| Parameter | Type | Default |
|
|
58
|
+
| ------------------------- | ----------------------------------------------- | -------------- |
|
|
59
|
+
| `keys` | `string \| string[]` | required |
|
|
60
|
+
| `onTrigger` | `(e: KeyboardEvent, node: HTMLElement) => void` | `node.click()` |
|
|
61
|
+
| `options.preventDefault` | `boolean` | `true` |
|
|
62
|
+
| `options.stopPropagation` | `boolean` | `false` |
|
|
63
|
+
| `options.ignoreInputs` | `boolean` | `true` |
|
|
64
|
+
| `options.ignoreRepeat` | `boolean` | `true` |
|
|
65
|
+
|
|
66
|
+
Shortcut strings: `k`, `ctrl+s`, `shift+space`, `mod+/`. `mod` resolves to `⌘` on Mac and `Ctrl` elsewhere.
|
|
67
|
+
|
|
68
|
+
### `pressed(keys?, className?)`
|
|
69
|
+
|
|
70
|
+
Adds `className` while a matching key is held, removes it on keyup. If `keys` is omitted, reads from `aria-keyshortcuts` (set by `hotkey`).
|
|
71
|
+
|
|
72
|
+
| Parameter | Type | Default |
|
|
73
|
+
| ----------- | -------------------- | ------------------------- |
|
|
74
|
+
| `keys` | `string \| string[]` | reads `aria-keyshortcuts` |
|
|
75
|
+
| `className` | `string` | `"is-pressed"` |
|
|
76
|
+
|
|
77
|
+
### `formatHint(keys)`
|
|
78
|
+
|
|
79
|
+
Formats a shortcut for display. `mod+s` → `⌘ S` on Mac, `Ctrl + S` on PC.
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
formatHint("mod+s"); // "⌘ S" / "Ctrl + S"
|
|
83
|
+
formatHint(["j", "down"]); // "J / Down"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### `addTransform(fn)`
|
|
87
|
+
|
|
88
|
+
Registers a transform applied before parsing and formatting. Returns a remover function.
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
const remove = addTransform((s) => s.replaceAll("primary", "mod"));
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### `likelyWithKeyboard()`
|
|
95
|
+
|
|
96
|
+
Returns `true` when the primary pointer is not coarse (i.e. likely has a keyboard). Use to conditionally render shortcut hints.
|
|
97
|
+
|
|
98
|
+
## Notes
|
|
99
|
+
|
|
100
|
+
- Shortcuts are global by default (fire regardless of which element has focus).
|
|
101
|
+
- Inputs, textareas, selects, and `contenteditable` are ignored by default.
|
|
102
|
+
- Add `data-keyshortcuts-ignore` to a container to suppress shortcuts while focus is inside that subtree.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Attachment } from "svelte/attachments";
|
|
2
|
+
|
|
3
|
+
//#region src/attachments.d.ts
|
|
4
|
+
interface HotkeyOptions {
|
|
5
|
+
preventDefault?: boolean;
|
|
6
|
+
stopPropagation?: boolean;
|
|
7
|
+
ignoreInputs?: boolean;
|
|
8
|
+
ignoreRepeat?: boolean;
|
|
9
|
+
}
|
|
10
|
+
type HotkeyTrigger = (e: KeyboardEvent, node: HTMLElement) => void;
|
|
11
|
+
/**
|
|
12
|
+
* Attachment that fires `onTrigger` (or `node.click()`) when one of `keys` is pressed.
|
|
13
|
+
* Sets `aria-keyshortcuts` on the element and restores the prior value on cleanup.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```svelte
|
|
17
|
+
* <button {@attach hotkey("mod+s", save)}>Save</button>
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
declare function hotkey(keys: string | string[], onTrigger?: HotkeyTrigger, options?: HotkeyOptions): Attachment<HTMLElement>;
|
|
21
|
+
/**
|
|
22
|
+
* Attachment that adds `className` while one of `keys` is held, removes it on keyup.
|
|
23
|
+
* If `keys` is omitted, reads the shortcut from `aria-keyshortcuts` (set by `hotkey`).
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```svelte
|
|
27
|
+
* <button {@attach hotkey("mod+s", save)} {@attach pressed("mod+s")}>Save</button>
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
declare function pressed(keys?: string | string[], className?: string): Attachment<HTMLElement>;
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region src/hint.d.ts
|
|
33
|
+
/**
|
|
34
|
+
* Format a shortcut string (or array of alternatives) into a human-readable hint.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* formatHint("mod+s") // → "⌘ S" on Mac, "Ctrl + S" on PC
|
|
38
|
+
* formatHint(["j", "down"]) // → "J / Down"
|
|
39
|
+
*/
|
|
40
|
+
declare function formatHint(shortcut: string | string[]): string;
|
|
41
|
+
/** Returns true when the primary pointing device is likely a mouse/trackpad (not touch-only). SSR-safe. */
|
|
42
|
+
declare function likelyWithKeyboard(): boolean;
|
|
43
|
+
//#endregion
|
|
44
|
+
//#region src/dispatcher.d.ts
|
|
45
|
+
type Transform = (shortcut: string) => string;
|
|
46
|
+
declare function addTransform(fn: Transform): () => void;
|
|
47
|
+
//#endregion
|
|
48
|
+
export { type HotkeyOptions, type HotkeyTrigger, addTransform, formatHint, hotkey, likelyWithKeyboard, pressed };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{on as e}from"svelte/events";const t={esc:`escape`,return:`enter`,del:`delete`,space:` `,up:`arrowup`,down:`arrowdown`,left:`arrowleft`,right:`arrowright`,option:`alt`,command:`meta`,win:`meta`,plus:`+`},n=new Set([`ctrl`,`shift`,`alt`,`meta`,`cmd`,`mod`]),r=typeof navigator<`u`&&/Mac|iPod|iPhone|iPad/.test(navigator.userAgent);function i(e){let n=e.toLowerCase();return t[n]??n}function a(e){let t=e.split(`+`).map(e=>i(e.trim())).filter(Boolean),a={key:``,ctrl:!1,shift:!1,alt:!1,meta:!1};for(let e of t){if(!n.has(e)){a.key=e;continue}e===`ctrl`?a.ctrl=!0:e===`shift`?a.shift=!0:e===`alt`?a.alt=!0:e===`meta`||e===`cmd`?a.meta=!0:e===`mod`&&(r?a.meta=!0:a.ctrl=!0)}return a}function o(e){return e.trim().split(/\s+/).filter(Boolean).map(a)}function s(e){return Array.isArray(e)?e.join(` `):e}function c(e,t){return i(e.key)===t.key&&e.ctrlKey===t.ctrl&&e.shiftKey===t.shift&&e.altKey===t.alt&&e.metaKey===t.meta}const l=new Set([`INPUT`,`TEXTAREA`,`SELECT`]),u=new Set,d=[];let f=null;const p=e=>{let t=e.target;return!!t&&(l.has(t.tagName)||t.isContentEditable)},m=e=>{let t=e.target.parentElement;for(;t;){if(t.hasAttribute(`data-keyshortcuts-ignore`))return document.activeElement!==t;t=t.parentElement}return!1},h=e=>{let t;for(let n of u)n.ignoreRepeat&&e.repeat||n.ignoreInputs&&p(e)||(t??=m(e),!t&&n.shortcuts.some(t=>c(e,t))&&n.handler(e))};function g(t){return u.size===0&&(f=e(window,`keydown`,h)),u.add(t),()=>{u.delete(t),u.size===0&&f&&(f(),f=null)}}function _(e){return d.push(e),()=>{let t=d.indexOf(e);t!==-1&&d.splice(t,1)}}function v(e){return d.reduce((e,t)=>t(e),e)}function y(e){return o(v(e))}function b(e,t,n){return r=>{let{preventDefault:i=!0,stopPropagation:a=!1,ignoreInputs:o=!0,ignoreRepeat:c=!0}=n??{},l=s(e),u=r.getAttribute(`aria-keyshortcuts`);r.setAttribute(`aria-keyshortcuts`,l);let d=g({shortcuts:y(l),handler:e=>{i&&e.preventDefault(),a&&e.stopPropagation(),t?t(e,r):r.click()},ignoreInputs:o,ignoreRepeat:c});return()=>{d(),u===null?r.removeAttribute(`aria-keyshortcuts`):r.setAttribute(`aria-keyshortcuts`,u)}}}function x(t,n=`is-pressed`){return r=>{let i=t===void 0?r.getAttribute(`aria-keyshortcuts`)??``:s(t);if(!i)return;let a=y(i),o=!1,l=()=>{o=!1,r.classList.remove(n)},u=g({shortcuts:a,handler:()=>{o=!0,r.classList.add(n)},ignoreInputs:!0,ignoreRepeat:!0}),d=e(window,`keyup`,e=>{o&&a.some(t=>c(e,t))&&l()}),f=e(window,`blur`,l);return()=>{u(),d(),f(),l()}}}const S={ctrl:[`⌃`,`Ctrl`],shift:[`⇧`,`Shift`],alt:[`⌥`,`Alt`],meta:[`⌘`,`Win`]},C={" ":`Space`,arrowup:`Up`,arrowdown:`Down`,arrowleft:`Left`,arrowright:`Right`,escape:`Esc`,enter:`Enter`,delete:`Delete`,backspace:`Backspace`,tab:`Tab`,home:`Home`,end:`End`,pageup:`PageUp`,pagedown:`PageDown`},w=typeof window<`u`?matchMedia(`(pointer: coarse)`):null;function T(e){return y(s(e)).map(e=>{let t=[];for(let n of[`ctrl`,`shift`,`alt`,`meta`])e[n]&&t.push(S[n][+!r]);let n=e.key;return t.push(C[n]??(n.length===1?n.toUpperCase():n)),t.join(r?` `:` + `)}).join(` / `)}function E(){return w?!w.matches:!1}export{_ as addTransform,T as formatHint,b as hotkey,E as likelyWithKeyboard,x as pressed};
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@joknoll/svelte-attach-key",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A Svelte attachment for binding keyboard shortcuts to DOM elements.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"attachment",
|
|
7
|
+
"hotkey",
|
|
8
|
+
"key",
|
|
9
|
+
"keymap",
|
|
10
|
+
"svelte",
|
|
11
|
+
"svelte5"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://github.com/joknoll/svelte-attach/tree/main/packages/key#readme",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/joknoll/svelte-attach/issues"
|
|
16
|
+
},
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"author": "joknoll",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/joknoll/svelte-attach.git",
|
|
22
|
+
"directory": "packages/key"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist"
|
|
26
|
+
],
|
|
27
|
+
"type": "module",
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"exports": {
|
|
30
|
+
".": "./dist/index.js",
|
|
31
|
+
"./package.json": "./package.json"
|
|
32
|
+
},
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^25.6.2",
|
|
38
|
+
"@typescript/native-preview": "7.0.0-dev.20260509.2",
|
|
39
|
+
"bumpp": "^11.1.0",
|
|
40
|
+
"svelte": "^5.55.0",
|
|
41
|
+
"typescript": "^6.0.3",
|
|
42
|
+
"vite-plus": "latest"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"svelte": ">=5.55.0"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "vp pack",
|
|
49
|
+
"dev": "vp pack --watch",
|
|
50
|
+
"test": "vp test --passWithNoTests",
|
|
51
|
+
"check": "vp check"
|
|
52
|
+
}
|
|
53
|
+
}
|