345 lines
9.5 KiB
Markdown
345 lines
9.5 KiB
Markdown
# Keyboard Support - Test Instructions
|
|
|
|
## ⚠️ Breaking Change
|
|
|
|
**Version 2.0** uses HTMX configuration objects instead of simple URL strings. The old format is **not supported**.
|
|
|
|
**Old format (no longer supported)**:
|
|
```javascript
|
|
{"A": "/url"}
|
|
```
|
|
|
|
**New format (required)**:
|
|
```javascript
|
|
{"A": {"hx-post": "/url"}}
|
|
```
|
|
|
|
## Files
|
|
|
|
- `keyboard_support.js` - Main keyboard support library with smart timeout logic
|
|
- `test_keyboard_support.html` - Test page to verify functionality
|
|
|
|
## Key Features
|
|
|
|
### Multiple Simultaneous Triggers
|
|
|
|
**IMPORTANT**: If multiple elements listen to the same combination, **ALL** of them will be triggered:
|
|
|
|
```javascript
|
|
add_keyboard_support('modal', '{"esc": "/close-modal"}');
|
|
add_keyboard_support('editor', '{"esc": "/cancel-edit"}');
|
|
add_keyboard_support('sidebar', '{"esc": "/hide-sidebar"}');
|
|
|
|
// Pressing ESC will trigger all 3 URLs simultaneously
|
|
```
|
|
|
|
This is crucial for use cases like the ESC key, which often needs to cancel multiple actions at once (close modal, cancel edit, hide panels, etc.).
|
|
|
|
### Smart Timeout Logic (Longest Match)
|
|
|
|
The library uses **a single global timeout** based on the sequence state, not on individual elements:
|
|
|
|
**Key principle**: If **any element** has a longer sequence possible, **all matching elements wait**.
|
|
|
|
Examples:
|
|
|
|
**Example 1**: Three elements, same combination
|
|
```javascript
|
|
add_keyboard_support('elem1', '{"esc": "/url1"}');
|
|
add_keyboard_support('elem2', '{"esc": "/url2"}');
|
|
add_keyboard_support('elem3', '{"esc": "/url3"}');
|
|
// Press ESC → ALL 3 trigger immediately (no longer sequences exist)
|
|
```
|
|
|
|
**Example 2**: Mixed - one has longer sequence
|
|
```javascript
|
|
add_keyboard_support('elem1', '{"A": "/url1"}');
|
|
add_keyboard_support('elem2', '{"A": "/url2"}');
|
|
add_keyboard_support('elem3', '{"A": "/url3", "A B": "/url3b"}');
|
|
// Press A:
|
|
// - elem3 has "A B" possible → EVERYONE WAITS 500ms
|
|
// - If B arrives: only elem3 triggers with "A B"
|
|
// - If timeout expires: elem1, elem2, elem3 ALL trigger with "A"
|
|
```
|
|
|
|
**Example 3**: Different combinations
|
|
```javascript
|
|
add_keyboard_support('elem1', '{"A B": "/url1"}');
|
|
add_keyboard_support('elem2', '{"C D": "/url2"}');
|
|
// Press A: elem1 waits for B, elem2 not affected
|
|
// Press C: elem2 waits for D, elem1 not affected
|
|
```
|
|
|
|
The timeout is tied to the **sequence being typed**, not to individual elements.
|
|
|
|
### Smart Timeout Logic (Longest Match)
|
|
|
|
Keyboard shortcuts are **disabled** when typing in input fields:
|
|
- `<input>` elements
|
|
- `<textarea>` elements
|
|
- Any `contenteditable` element
|
|
|
|
This ensures normal typing (Ctrl+C, Ctrl+A, etc.) works as expected in forms.
|
|
|
|
## How to Test
|
|
|
|
1. **Download both files** to the same directory
|
|
2. **Open `test_keyboard_support.html`** in a web browser
|
|
3. **Try the configured shortcuts**:
|
|
- `a` - Simple key (waits if "A B" might follow)
|
|
- `Ctrl+S` - Save combination (immediate)
|
|
- `Ctrl+C` - Copy combination (waits because "Ctrl+C C" exists)
|
|
- `A B` - Sequence (waits because "A B C" exists)
|
|
- `A B C` - Triple sequence (triggers immediately)
|
|
- `Ctrl+C C` - Press Ctrl+C together, release, then press C alone
|
|
- `Ctrl+C Ctrl+C` - Press Ctrl+C, keep Ctrl, release C, press C again
|
|
- `shift shift` - Press Shift twice in sequence
|
|
- `esc` - Escape key (immediate)
|
|
|
|
4. **Test focus behavior**:
|
|
- Click on the test element to focus it (turns blue)
|
|
- Try shortcuts with focus
|
|
- Click outside to remove focus
|
|
- Try shortcuts without focus
|
|
- The log shows whether the element had focus when triggered
|
|
|
|
5. **Test input protection**:
|
|
- Try typing in the input field
|
|
- Use Ctrl+C, Ctrl+A, etc. - should work normally
|
|
- Shortcuts should NOT trigger while typing in input
|
|
|
|
## Expected Behavior
|
|
|
|
### Smart Timeout Examples
|
|
|
|
**Scenario 1**: Only "A" is configured (no element has "A B")
|
|
- Press A → Triggers **immediately**
|
|
|
|
**Scenario 2**: At least one element has "A B"
|
|
- Press A → **ALL elements with "A" wait 500ms**
|
|
- If B pressed within 500ms → Only elements with "A B" trigger
|
|
- If timeout expires → ALL elements with "A" trigger
|
|
|
|
**Scenario 3**: "A", "A B", and "A B C" all configured (same or different elements)
|
|
- Press A → Waits (because "A B" exists)
|
|
- Press B → Waits (because "A B C" exists)
|
|
- Press C → Triggers "A B C" **immediately**
|
|
|
|
**Scenario 4**: Multiple elements, ESC on all
|
|
```javascript
|
|
add_keyboard_support('modal', '{"esc": "/close"}');
|
|
add_keyboard_support('editor', '{"esc": "/cancel"}');
|
|
```
|
|
- Press ESC → **Both trigger simultaneously** (no longer sequences)
|
|
|
|
## Integration in Your Project
|
|
|
|
## Integration in Your Project
|
|
|
|
### Configuration Format
|
|
|
|
The library now uses **HTMX configuration objects** instead of simple URL strings:
|
|
|
|
```python
|
|
# New format with HTMX configuration
|
|
combinations = {
|
|
"Ctrl+S": {
|
|
"hx-post": "/save-url",
|
|
"hx-target": "#result",
|
|
"hx-swap": "innerHTML"
|
|
},
|
|
"A B": {
|
|
"hx-post": "/sequence-url",
|
|
"hx-vals": {"extra": "data"}
|
|
},
|
|
"esc": {
|
|
"hx-get": "/cancel-url"
|
|
}
|
|
}
|
|
|
|
# This will generate the JavaScript call
|
|
f"add_keyboard_support('{element_id}', '{json.dumps(combinations)}')"
|
|
```
|
|
|
|
### Supported HTMX Attributes
|
|
|
|
You can use any HTMX attribute in the configuration object:
|
|
|
|
**HTTP Methods** (one required):
|
|
- `hx-post` - POST request
|
|
- `hx-get` - GET request
|
|
- `hx-put` - PUT request
|
|
- `hx-delete` - DELETE request
|
|
- `hx-patch` - PATCH request
|
|
|
|
**Common Options**:
|
|
- `hx-target` - Target element selector
|
|
- `hx-swap` - Swap strategy (innerHTML, outerHTML, etc.)
|
|
- `hx-vals` - Additional values to send (object)
|
|
- `hx-headers` - Custom headers (object)
|
|
- `hx-select` - Select specific content from response
|
|
- `hx-confirm` - Confirmation message
|
|
|
|
All other `hx-*` attributes are supported and will be converted to the appropriate htmx.ajax() parameters.
|
|
|
|
### Automatic Parameters
|
|
|
|
The library automatically adds these parameters to every request:
|
|
- `combination` - The combination that triggered the action (e.g., "Ctrl+S")
|
|
- `has_focus` - Boolean indicating if the element had focus
|
|
- `is_inside` - Boolean indicating if the focus is inside the element (element itself or any child)
|
|
|
|
Example final request:
|
|
```javascript
|
|
htmx.ajax('POST', '/save-url', {
|
|
target: '#result',
|
|
swap: 'innerHTML',
|
|
values: {
|
|
extra: "data", // from hx-vals
|
|
combination: "Ctrl+S", // automatic
|
|
has_focus: true, // automatic
|
|
is_inside: true // automatic
|
|
}
|
|
})
|
|
```
|
|
|
|
## Integration Examples
|
|
|
|
### Basic Usage
|
|
|
|
```python
|
|
combinations = {
|
|
"Ctrl+S": {
|
|
"hx-post": "/save"
|
|
}
|
|
}
|
|
```
|
|
|
|
### With Target and Swap
|
|
|
|
```python
|
|
combinations = {
|
|
"Ctrl+D": {
|
|
"hx-delete": "/item",
|
|
"hx-target": "#item-list",
|
|
"hx-swap": "outerHTML"
|
|
}
|
|
}
|
|
```
|
|
|
|
### With Extra Values
|
|
|
|
```python
|
|
combinations = {
|
|
"Ctrl+N": {
|
|
"hx-post": "/create",
|
|
"hx-vals": json.dumps({"type": "quick", "mode": "keyboard"})
|
|
}
|
|
}
|
|
```
|
|
|
|
### Multiple Elements Example
|
|
|
|
```python
|
|
# Modal close
|
|
modal_combinations = {
|
|
"esc": {
|
|
"hx-post": "/modal/close",
|
|
"hx-target": "#modal",
|
|
"hx-swap": "outerHTML"
|
|
}
|
|
}
|
|
|
|
# Editor cancel
|
|
editor_combinations = {
|
|
"esc": {
|
|
"hx-post": "/editor/cancel",
|
|
"hx-target": "#editor",
|
|
"hx-swap": "innerHTML"
|
|
}
|
|
}
|
|
|
|
# Both will trigger when ESC is pressed
|
|
f"add_keyboard_support('modal', '{json.dumps(modal_combinations)}')"
|
|
f"add_keyboard_support('editor', '{json.dumps(editor_combinations)}')"
|
|
```
|
|
|
|
### Removing Keyboard Support
|
|
|
|
When you no longer need keyboard support for an element:
|
|
|
|
```python
|
|
# Remove keyboard support
|
|
f"remove_keyboard_support('{element_id}')"
|
|
```
|
|
|
|
**Behavior**:
|
|
- Removes the element from the keyboard registry
|
|
- If this was the last element, automatically detaches global event listeners
|
|
- Cleans up all associated state (timeouts, snapshots, etc.)
|
|
- Other elements continue to work normally
|
|
|
|
**Example**:
|
|
```javascript
|
|
// Add support
|
|
add_keyboard_support('modal', '{"esc": {"hx-post": "/close"}}');
|
|
|
|
// Later, remove support
|
|
remove_keyboard_support('modal');
|
|
// If no other elements remain, keyboard listeners are completely removed
|
|
```
|
|
|
|
## API Reference
|
|
|
|
### add_keyboard_support(elementId, combinationsJson)
|
|
|
|
Adds keyboard support to an element.
|
|
|
|
**Parameters**:
|
|
- `elementId` (string): ID of the HTML element
|
|
- `combinationsJson` (string): JSON string of combinations with HTMX configs
|
|
|
|
**Returns**: void
|
|
|
|
### remove_keyboard_support(elementId)
|
|
|
|
Removes keyboard support from an element.
|
|
|
|
**Parameters**:
|
|
- `elementId` (string): ID of the HTML element
|
|
|
|
**Returns**: void
|
|
|
|
**Side effects**:
|
|
- If last element: detaches global event listeners and cleans up all state
|
|
|
|
## Technical Details
|
|
|
|
### Trie-based Matching
|
|
|
|
The library uses a prefix tree (trie) data structure:
|
|
- Each node represents a keyboard snapshot (set of pressed keys)
|
|
- Leaf nodes contain the HTMX configuration object
|
|
- Intermediate nodes indicate longer sequences exist
|
|
- Enables efficient O(n) matching where n is sequence length
|
|
|
|
### HTMX Integration
|
|
|
|
Configuration objects are mapped to htmx.ajax() calls:
|
|
- `hx-*` attributes are converted to camelCase parameters
|
|
- HTTP method is extracted from `hx-post`, `hx-get`, etc.
|
|
- `combination`, `has_focus`, and `is_inside` are automatically added to values
|
|
- All standard HTMX options are supported
|
|
|
|
### Key Normalization
|
|
|
|
- Case-insensitive: "ctrl" = "Ctrl" = "CTRL"
|
|
- Mapped keys: "Control" → "ctrl", "Escape" → "esc", "Delete" → "del"
|
|
- Simultaneous keys represented as sorted sets
|
|
|
|
## Notes
|
|
|
|
- The test page mocks `htmx.ajax` to display results in the console
|
|
- In production, real AJAX calls will be made to your backend
|
|
- Sequence timeout is 500ms between keys
|
|
- Maximum 10 snapshots kept in history to prevent memory issues |