Added first controls
This commit is contained in:
345
docs/Keyboard Support.md
Normal file
345
docs/Keyboard Support.md
Normal file
@@ -0,0 +1,345 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user