Implemented enable/disable for keyboard support

This commit is contained in:
2026-03-15 08:33:39 +01:00
parent f773fd1611
commit feb9da50b2
7 changed files with 151 additions and 54 deletions

View File

@@ -100,6 +100,57 @@ add_keyboard_support('elem2', '{"C D": "/url2"}');
The timeout is tied to the **sequence being typed**, not to individual elements.
### Enabling and Disabling Combinations
Each combination can be enabled or disabled independently. A disabled combination is registered and tracked, but its action is never triggered.
| State | Behavior |
|-------|----------|
| `enabled=True` (default) | Combination triggers normally |
| `enabled=False` | Combination is silently ignored when pressed |
**Setting the initial state at registration:**
```python
# Enabled by default
keyboard.add("ctrl+s", self.commands.save())
# Disabled at startup
keyboard.add("ctrl+d", self.commands.delete(), enabled=False)
```
**Toggling dynamically at runtime:**
Use `mk_enable()` and `mk_disable()` to change the state from a server response. Both methods return an out-of-band HTMX element (`hx-swap-oob`) that updates the DOM without a full page reload.
```python
# Enable a combination (e.g., once an item is selected)
def handle_select(self):
item = ...
return item.render(), self.keyboard.mk_enable("ctrl+d")
# Disable a combination (e.g., when nothing is selected)
def handle_deselect(self):
return self.keyboard.mk_disable("ctrl+d")
```
The enabled state is stored in a hidden control `<div>` rendered alongside the keyboard script. The JavaScript reads this state before triggering any action.
**State at a glance:**
```
┌─────────────────────────────────────┐
│ Keyboard control div (hidden) │
│ ┌──────────────────────────────┐ │
│ │ div [data-combination="esc"] │ │
│ │ [data-enabled="true"] │ │
│ ├──────────────────────────────┤ │
│ │ div [data-combination="ctrl+d"] │ │
│ │ [data-enabled="false"] │ │
│ └──────────────────────────────┘ │
└─────────────────────────────────────┘
```
### Smart Timeout Logic (Longest Match)
Keyboard shortcuts are **disabled** when typing in input fields:
@@ -364,16 +415,56 @@ remove_keyboard_support('modal');
## API Reference
### add_keyboard_support(elementId, combinationsJson)
### add_keyboard_support(elementId, controlDivId, combinationsJson)
Adds keyboard support to an element.
**Parameters**:
- `elementId` (string): ID of the HTML element
- `elementId` (string): ID of the HTML element to watch for key events
- `controlDivId` (string): ID of the keyboard control div rendered by `Keyboard.render()`, used to look up enabled/disabled state at runtime
- `combinationsJson` (string): JSON string of combinations with HTMX configs
**Returns**: void
### Python Component Methods
These methods are available on the `Keyboard` instance.
#### add(sequence, command, require_inside=True, enabled=True)
Registers a key combination.
| Parameter | Type | Description | Default |
|-----------|------|-------------|---------|
| `sequence` | `str` | Key combination string, e.g. `"ctrl+s"`, `"esc"`, `"a b"` | — |
| `command` | `Command` | Command to execute when the combination is triggered | — |
| `require_inside` | `bool` | If `True`, only triggers when focus is inside the element | `True` |
| `enabled` | `bool` | Whether the combination is active at render time | `True` |
**Returns**: `self` (chainable)
#### mk_enable(sequence)
Returns an out-of-band HTMX element that enables a combination at runtime.
| Parameter | Type | Description |
|-----------|------|-------------|
| `sequence` | `str` | Key combination to enable, must match exactly what was passed to `add()` |
**Returns**: `Div` with `hx-swap-oob="true"`
#### mk_disable(sequence)
Returns an out-of-band HTMX element that disables a combination at runtime.
| Parameter | Type | Description |
|-----------|------|-------------|
| `sequence` | `str` | Key combination to disable, must match exactly what was passed to `add()` |
**Returns**: `Div` with `hx-swap-oob="true"`
---
### remove_keyboard_support(elementId)
Removes keyboard support from an element.

View File

@@ -181,13 +181,13 @@ Users can rename nodes via the edit button:
```python
# Programmatically start rename
tree._start_rename("node-id")
tree.handle_start_rename("node-id")
# Save rename
tree._save_rename("node-id", "New Label")
tree.handle_save_rename("node-id", "New Label")
# Cancel rename
tree._cancel_rename()
tree.handle_cancel_rename()
```
### Deleting Nodes
@@ -201,7 +201,7 @@ Users can delete nodes via the delete button:
```python
# Programmatically delete node
tree._delete_node("node-id") # Raises ValueError if node has children
tree.handle_delete_node("node-id") # Raises ValueError if node has children
```
## Content System
@@ -449,18 +449,20 @@ tree = TreeView(parent=root_instance, _id="dynamic-tree")
root = TreeNode(id="root", label="Tasks", type="folder")
tree.add_node(root)
# Function to handle selection
def on_node_selected(node_id):
# Custom logic when node is selected
node = tree._state.items[node_id]
tree._select_node(node_id)
# Custom logic when node is selected
node = tree._state.items[node_id]
tree.handle_select_node(node_id)
# Update a detail panel elsewhere in the UI
return Div(
H3(f"Selected: {node.label}"),
P(f"Type: {node.type}"),
P(f"Children: {len(node.children)}")
)
# Update a detail panel elsewhere in the UI
return Div(
H3(f"Selected: {node.label}"),
P(f"Type: {node.type}"),
P(f"Children: {len(node.children)}")
)
# Override select command with custom handler
# (In practice, you'd extend the Commands class or use event callbacks)