I can select range with visual feedback

This commit is contained in:
2026-02-10 23:00:45 +01:00
parent 79c37493af
commit 520a8914fc
7 changed files with 353 additions and 25 deletions

View File

@@ -19,6 +19,13 @@ The mouse support library provides keyboard-like binding capabilities for mouse
- `ctrl+shift+click` - Multiple modifiers
- Any combination of modifiers
**Drag Actions**:
- `mousedown>mouseup` - Left button drag (press, drag at least 5px, release)
- `rmousedown>mouseup` - Right button drag
- `ctrl+mousedown>mouseup` - Ctrl + left drag
- `shift+mousedown>mouseup` - Shift + left drag
- Any combination of modifiers
**Sequences**:
- `click right_click` (or `click rclick`) - Click then right-click within 500ms
- `click click` - Double click sequence
@@ -128,6 +135,127 @@ function getCellId(event) {
}
```
## Drag Actions (mousedown>mouseup)
### How It Works
Drag detection uses a **5-pixel threshold**: the action only activates when the mouse has moved at least 5px after mousedown. This prevents accidental drags from normal clicks.
**Lifecycle**:
1. `mousedown` → library waits, stores start position
2. Mouse moves > 5px → drag mode activated, `hx-vals-extra` function called with mousedown event → result stored
3. Mouse moves (during drag) → `on_move` function called on each animation frame *(if configured)*
4. `mouseup``hx-vals-extra` function called again with mouseup event → HTMX request fired with both values
5. The subsequent `click` event is suppressed (left button only)
### Two-Phase Values
For `mousedown>mouseup`, the `hx-vals-extra` function is called **twice** — once at each phase — and values are suffixed automatically:
```javascript
// hx-vals-extra function (same function, called twice)
function getCellId(event) {
const cell = event.target.closest('.dt2-cell');
return { cell_id: cell.id };
}
```
**Values sent to server**:
```json
{
"c_id": "command_id",
"cell_id_mousedown": "tcell_grid-0-2",
"cell_id_mouseup": "tcell_grid-3-5",
"combination": "mousedown>mouseup",
"is_inside": true,
"has_focus": false
}
```
**Python handler**:
```python
def on_mouse_selection(self, combination, is_inside, cell_id_mousedown, cell_id_mouseup):
# cell_id_mousedown: where the drag started
# cell_id_mouseup: where the drag ended
...
```
### Real-Time Visual Feedback with `on_move`
The `on_move` attribute specifies a JavaScript function to call on each animation frame **during the drag**, enabling real-time visual feedback without any server calls.
**Configuration**:
```javascript
{
"mousedown>mouseup": {
"hx-post": "/myfasthtml/commands",
"hx-vals": {"c_id": "command_id"},
"hx-vals-extra": {"js": "getCellId"},
"on-move": "onDragMove" // called during drag
}
}
```
**`on_move` function signature**:
```javascript
function onDragMove(event, combination, mousedown_result) {
// event : current mousemove event
// combination : e.g. "mousedown>mouseup" or "ctrl+mousedown>mouseup"
// mousedown_result : raw result of hx-vals-extra at mousedown (unsuffixed), or null
}
```
**Key properties**:
- Called only after the 5px drag threshold is exceeded (never during a simple click)
- Throttled via `requestAnimationFrame` (~60fps) — no manual throttling needed
- Return value is ignored
- Visual state cleanup is handled by the server response (which overwrites any client-side visual)
**DataGrid range selection example**:
```javascript
function highlightDragRange(event, combination, mousedownResult) {
const startCell = mousedownResult ? mousedownResult.cell_id : null;
const endCell = event.target.closest('.dt2-cell');
if (!startCell || !endCell) return;
// Clear previous preview
document.querySelectorAll('.dt2-drag-preview')
.forEach(el => el.classList.remove('dt2-drag-preview'));
// Highlight range from start to current cell
applyRangeClass(startCell, endCell.id, 'dt2-drag-preview');
}
```
**Canvas selection rectangle example**:
```javascript
function drawSelectionRect(event, combination, mousedownResult) {
if (!mousedownResult) return;
const canvas = document.getElementById('my-canvas');
const ctx = canvas.getContext('2d');
const rect = canvas.getBoundingClientRect();
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.strokeStyle = 'blue';
ctx.strokeRect(
mousedownResult.x - rect.left,
mousedownResult.y - rect.top,
event.clientX - rect.left - (mousedownResult.x - rect.left),
event.clientY - rect.top - (mousedownResult.y - rect.top)
);
}
```
**Python configuration**:
```python
mouse.add(
"mousedown>mouseup",
selection_command,
hx_vals="js:getCellId()",
on_move="js:highlightDragRange()"
)
```
## API Reference
### add_mouse_support(elementId, combinationsJson)
@@ -237,20 +365,23 @@ mouse.add("right_click", context_menu_command)
def add(self, sequence: str, command: Command = None, *,
hx_post: str = None, hx_get: str = None, hx_put: str = None,
hx_delete: str = None, hx_patch: str = None,
hx_target: str = None, hx_swap: str = None, hx_vals=None)
hx_target: str = None, hx_swap: str = None, hx_vals=None,
on_move: str = None)
```
**Parameters**:
- `sequence`: Mouse event sequence (e.g., "click", "ctrl+click", "click right_click")
- `sequence`: Mouse event sequence (e.g., "click", "ctrl+click", "mousedown>mouseup")
- `command`: Optional Command object for server-side action
- `hx_post`, `hx_get`, etc.: HTMX URL parameters (override command)
- `hx_target`: HTMX target selector (overrides command)
- `hx_swap`: HTMX swap strategy (overrides command)
- `hx_vals`: Additional HTMX values - dict or "js:functionName()" for dynamic values
- `hx_vals`: Additional HTMX values - dict or `"js:functionName()"` for dynamic values
- `on_move`: Client-side JS function called during drag — `"js:functionName()"` format. Only valid with `mousedown>mouseup` sequences.
**Note**:
- Named parameters (except `hx_vals`) override the command's parameters.
- `hx_vals` is **merged** with command's values (stored in `hx-vals-extra`), preserving `c_id`.
- `on_move` is purely client-side — it never triggers a server call.
### Usage Patterns
@@ -275,6 +406,16 @@ mouse.add("right_click", hx_post="/context-menu", hx_target="#menu", hx_swap="in
mouse.add("shift+click", my_command, hx_vals="js:getClickPosition()")
```
**With drag and real-time feedback**:
```python
mouse.add(
"mousedown>mouseup",
selection_command,
hx_vals="js:getCellId()",
on_move="js:highlightDragRange()"
)
```
### Sequences
```python
@@ -558,6 +699,43 @@ const combinations = {
};
```
### Range Selection with Visual Feedback
```python
# Python: configure drag with live feedback
mouse.add(
"mousedown>mouseup",
self.commands.on_mouse_selection(),
hx_vals="js:getCellId()",
on_move="js:highlightDragRange()"
)
```
```javascript
// JavaScript: real-time highlight during drag
function highlightDragRange(event, combination, mousedownResult) {
const startCell = mousedownResult ? mousedownResult.cell_id : null;
const endCell = event.target.closest('.dt2-cell');
if (!startCell || !endCell) return;
document.querySelectorAll('.dt2-drag-preview')
.forEach(el => el.classList.remove('dt2-drag-preview'));
applyRangeClass(startCell, endCell.id, 'dt2-drag-preview');
// Server response will replace .dt2-drag-preview with final selection classes
}
```
```python
# Python: server handler receives both positions
def on_mouse_selection(self, combination, is_inside, cell_id_mousedown, cell_id_mouseup):
if is_inside and cell_id_mousedown and cell_id_mouseup:
pos_start = self._get_pos_from_element_id(cell_id_mousedown)
pos_end = self._get_pos_from_element_id(cell_id_mouseup)
self._state.selection.set_range(pos_start, pos_end)
return self.render_partial()
```
## Troubleshooting
### Clicks not detected
@@ -578,14 +756,29 @@ const combinations = {
- Check if longer sequences exist (causes waiting)
- Verify the combination string format (space-separated)
### Drag not triggering
- Ensure the mouse moved at least 5px before releasing
- Verify `mousedown>mouseup` (not `mousedown_mouseup`) in the combination string
- Check that `hx-vals-extra` function exists and is accessible via `window`
### `on_move` not called
- Verify `on_move` is only used with `mousedown>mouseup` sequences
- Check that the function name matches exactly (case-sensitive)
- Ensure the function is accessible via `window` (not inside a module scope)
- Remember: `on_move` only fires after the 5px threshold — it won't fire on small movements
## Technical Details
### Architecture
- **Global listeners** on `document` for `click` and `contextmenu` events
- **Global listeners** on `document` for `click`, `contextmenu`, `mousedown`, `mouseup` events
- **Tree-based matching** using prefix trees (same as keyboard support)
- **Single timeout** for all elements (sequence-based, not element-based)
- **Independent from keyboard support** (separate registry and timeouts)
- **Drag detection**: temporary `mousemove` listener attached on `mousedown`, removed when 5px threshold exceeded
- **`on_move` throttling**: `requestAnimationFrame` used internally — no manual throttling needed in user functions
### Performance