I can select range with visual feedback
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user