608 lines
16 KiB
Markdown
608 lines
16 KiB
Markdown
# Mouse Support - Documentation
|
|
|
|
## Overview
|
|
|
|
The mouse support library provides keyboard-like binding capabilities for mouse actions. It supports simple clicks, modified clicks (with Ctrl/Shift/Alt), and sequences of clicks with smart timeout logic.
|
|
|
|
## Features
|
|
|
|
### Supported Mouse Actions
|
|
|
|
**Basic Actions**:
|
|
- `click` - Left click
|
|
- `right_click` (or `rclick`) - Right click (contextmenu)
|
|
|
|
**Modified Actions**:
|
|
- `ctrl+click` (or `ctrl+rclick`) - Ctrl+Click (or Cmd+Click on Mac)
|
|
- `shift+click` (or `shift+rclick`) - Shift+Click
|
|
- `alt+click` (or `alt+rclick`) - Alt+Click
|
|
- `ctrl+shift+click` - Multiple modifiers
|
|
- Any combination of modifiers
|
|
|
|
**Sequences**:
|
|
- `click right_click` (or `click rclick`) - Click then right-click within 500ms
|
|
- `click click` - Double click sequence
|
|
- `ctrl+click click` - Ctrl+click then normal click
|
|
- Any sequence of actions
|
|
|
|
**Note**: `rclick` is an alias for `right_click` and can be used interchangeably.
|
|
|
|
### Smart Timeout Logic
|
|
|
|
Same as keyboard support:
|
|
- If **any element** has a longer sequence possible, **all matching elements wait**
|
|
- Timeout is 500ms between actions
|
|
- Immediate trigger if no longer sequences exist
|
|
|
|
### Multiple Element Support
|
|
|
|
Multiple elements can listen to the same mouse action and all will trigger simultaneously.
|
|
|
|
## Configuration Format
|
|
|
|
Uses HTMX configuration objects (same as keyboard support):
|
|
|
|
```javascript
|
|
const combinations = {
|
|
"click": {
|
|
"hx-post": "/handle-click",
|
|
"hx-target": "#result"
|
|
},
|
|
"ctrl+click": {
|
|
"hx-post": "/handle-ctrl-click",
|
|
"hx-swap": "innerHTML"
|
|
},
|
|
"rclick": { // Alias for right_click
|
|
"hx-post": "/context-menu"
|
|
},
|
|
"click rclick": { // Can use rclick in sequences too
|
|
"hx-post": "/sequence-action",
|
|
"hx-vals": {"type": "sequence"}
|
|
}
|
|
};
|
|
|
|
add_mouse_support('my-element', JSON.stringify(combinations));
|
|
```
|
|
|
|
### Dynamic Values with JavaScript Functions
|
|
|
|
You can add dynamic values computed at click time using `hx-vals-extra`. This is useful when combined with a Command (which provides `hx-vals` with `c_id`).
|
|
|
|
**Configuration format:**
|
|
```javascript
|
|
const combinations = {
|
|
"click": {
|
|
"hx-post": "/myfasthtml/commands",
|
|
"hx-vals": {"c_id": "command_id"}, // Static values from Command
|
|
"hx-vals-extra": {"js": "getClickData"} // Dynamic values via JS function
|
|
}
|
|
};
|
|
```
|
|
|
|
**How it works:**
|
|
1. `hx-vals` contains static values (e.g., `c_id` from Command)
|
|
2. `hx-vals-extra.dict` contains additional static values (merged)
|
|
3. `hx-vals-extra.js` specifies a function to call for dynamic values (merged)
|
|
|
|
**JavaScript function definition:**
|
|
```javascript
|
|
// Function receives (event, element, combinationStr)
|
|
function getClickData(event, element, combination) {
|
|
return {
|
|
x: event.clientX,
|
|
y: event.clientY,
|
|
target_id: event.target.id,
|
|
timestamp: Date.now()
|
|
};
|
|
}
|
|
```
|
|
|
|
The function parameters are optional - use what you need:
|
|
|
|
```javascript
|
|
// Full context
|
|
function getFullContext(event, element, combination) {
|
|
return { x: event.clientX, elem: element.id, combo: combination };
|
|
}
|
|
|
|
// Just the event
|
|
function getPosition(event) {
|
|
return { x: event.clientX, y: event.clientY };
|
|
}
|
|
|
|
// No parameters needed
|
|
function getTimestamp() {
|
|
return { ts: Date.now() };
|
|
}
|
|
```
|
|
|
|
**Built-in helper function:**
|
|
```javascript
|
|
// getCellId() - finds parent with .dt2-cell class and returns its id
|
|
function getCellId(event) {
|
|
const cell = event.target.closest('.dt2-cell');
|
|
if (cell && cell.id) {
|
|
return { cell_id: cell.id };
|
|
}
|
|
return {};
|
|
}
|
|
```
|
|
|
|
## API Reference
|
|
|
|
### add_mouse_support(elementId, combinationsJson)
|
|
|
|
Adds mouse support to an element.
|
|
|
|
**Parameters**:
|
|
- `elementId` (string): ID of the HTML element
|
|
- `combinationsJson` (string): JSON string of combinations with HTMX configs
|
|
|
|
**Returns**: void
|
|
|
|
**Example**:
|
|
```javascript
|
|
add_mouse_support('button1', JSON.stringify({
|
|
"click": {"hx-post": "/click"},
|
|
"ctrl+click": {"hx-post": "/ctrl-click"}
|
|
}));
|
|
```
|
|
|
|
### remove_mouse_support(elementId)
|
|
|
|
Removes mouse 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
|
|
|
|
**Example**:
|
|
```javascript
|
|
remove_mouse_support('button1');
|
|
```
|
|
|
|
## Automatic Parameters
|
|
|
|
The library automatically adds these parameters to every HTMX request:
|
|
- `combination` - The mouse combination that triggered the action (e.g., "ctrl+click")
|
|
- `has_focus` - Boolean indicating if the element had focus when clicked
|
|
- `is_inside` - Boolean indicating if the click was inside the element
|
|
- For `click`: `true` if clicked inside element, `false` if clicked outside
|
|
- For `right_click`: always `true` (only triggers when clicking on element)
|
|
- `has_focus` - Boolean indicating if the element had focus when the action triggered
|
|
- `clicked_inside` - Boolean indicating if the click was inside the element or outside
|
|
|
|
### Parameter Details
|
|
|
|
**`has_focus`**:
|
|
- `true` if the registered element currently has focus
|
|
- `false` otherwise
|
|
- Useful for knowing if the element was the active element
|
|
|
|
**`clicked_inside`**:
|
|
- For `click` actions: `true` if clicked on/inside the element, `false` if clicked outside
|
|
- For `right_click` actions: always `true` (since right-click only triggers on the element)
|
|
- Useful for "click outside to close" logic
|
|
|
|
**Example values sent**:
|
|
```javascript
|
|
// User clicks inside a modal
|
|
{
|
|
combination: "click",
|
|
has_focus: true,
|
|
clicked_inside: true
|
|
}
|
|
|
|
// User clicks outside the modal (modal still gets triggered because click is global)
|
|
{
|
|
combination: "click",
|
|
has_focus: false,
|
|
clicked_inside: false // Perfect for closing the modal!
|
|
}
|
|
|
|
// User right-clicks on an item
|
|
{
|
|
combination: "right_click",
|
|
has_focus: false,
|
|
clicked_inside: true // Always true for right_click
|
|
}
|
|
```
|
|
|
|
## Python Integration
|
|
|
|
### Mouse Class
|
|
|
|
The `Mouse` class provides a convenient way to add mouse support to elements.
|
|
|
|
```python
|
|
from myfasthtml.controls.Mouse import Mouse
|
|
from myfasthtml.core.commands import Command
|
|
|
|
# Create mouse support for an element
|
|
mouse = Mouse(parent_element)
|
|
|
|
# Add combinations
|
|
mouse.add("click", select_command)
|
|
mouse.add("ctrl+click", toggle_command)
|
|
mouse.add("right_click", context_menu_command)
|
|
```
|
|
|
|
### Mouse.add() Method
|
|
|
|
```python
|
|
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)
|
|
```
|
|
|
|
**Parameters**:
|
|
- `sequence`: Mouse event sequence (e.g., "click", "ctrl+click", "click right_click")
|
|
- `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
|
|
|
|
**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`.
|
|
|
|
### Usage Patterns
|
|
|
|
**With Command only**:
|
|
```python
|
|
mouse.add("click", my_command)
|
|
```
|
|
|
|
**With Command and overrides**:
|
|
```python
|
|
# Command provides hx-post, but we override the target
|
|
mouse.add("ctrl+click", my_command, hx_target="#other-result")
|
|
```
|
|
|
|
**Without Command (direct HTMX)**:
|
|
```python
|
|
mouse.add("right_click", hx_post="/context-menu", hx_target="#menu", hx_swap="innerHTML")
|
|
```
|
|
|
|
**With dynamic values**:
|
|
```python
|
|
mouse.add("shift+click", my_command, hx_vals="js:getClickPosition()")
|
|
```
|
|
|
|
### Sequences
|
|
|
|
```python
|
|
mouse = Mouse(element)
|
|
mouse.add("click", single_click_command)
|
|
mouse.add("click click", double_click_command)
|
|
mouse.add("click right_click", special_action_command)
|
|
```
|
|
|
|
### Multiple Elements
|
|
|
|
```python
|
|
# Each element gets its own Mouse instance
|
|
for item in items:
|
|
mouse = Mouse(item)
|
|
mouse.add("click", Command("select", "Select item", lambda i=item: select(i)))
|
|
mouse.add("ctrl+click", Command("toggle", "Toggle item", lambda i=item: toggle(i)))
|
|
```
|
|
|
|
### Dynamic hx-vals with JavaScript
|
|
|
|
You can use `"js:functionName()"` to call a client-side JavaScript function that returns additional values to send with the request. The command's `c_id` is preserved.
|
|
|
|
**Python**:
|
|
```python
|
|
mouse.add("click", my_command, hx_vals="js:getClickContext()")
|
|
```
|
|
|
|
**Generated config** (internally):
|
|
```json
|
|
{
|
|
"hx-post": "/myfasthtml/commands",
|
|
"hx-vals": {"c_id": "command_id"},
|
|
"hx-vals-extra": {"js": "getClickContext"}
|
|
}
|
|
```
|
|
|
|
**JavaScript** (client-side):
|
|
```javascript
|
|
// Function receives (event, element, combinationStr)
|
|
function getClickContext(event, element, combination) {
|
|
return {
|
|
x: event.clientX,
|
|
y: event.clientY,
|
|
elementId: element.id,
|
|
combo: combination
|
|
};
|
|
}
|
|
|
|
// Simple function - parameters are optional
|
|
function getTimestamp() {
|
|
return { ts: Date.now() };
|
|
}
|
|
```
|
|
|
|
**Values sent to server**:
|
|
```json
|
|
{
|
|
"c_id": "command_id",
|
|
"x": 150,
|
|
"y": 200,
|
|
"elementId": "my-element",
|
|
"combo": "click",
|
|
"combination": "click",
|
|
"has_focus": false,
|
|
"is_inside": true
|
|
}
|
|
```
|
|
|
|
You can also pass a static dict:
|
|
```python
|
|
mouse.add("click", my_command, hx_vals={"extra_key": "extra_value"})
|
|
```
|
|
|
|
### Low-Level Usage (without Mouse class)
|
|
|
|
For advanced use cases, you can generate the JavaScript directly:
|
|
|
|
```python
|
|
import json
|
|
|
|
combinations = {
|
|
"click": {
|
|
"hx-post": "/item/select"
|
|
},
|
|
"ctrl+click": {
|
|
"hx-post": "/item/select-multiple",
|
|
"hx-vals": {"mode": "multi"}
|
|
},
|
|
"right_click": {
|
|
"hx-post": "/item/context-menu",
|
|
"hx-target": "#context-menu",
|
|
"hx-swap": "innerHTML"
|
|
}
|
|
}
|
|
|
|
Script(f"add_mouse_support('{element_id}', '{json.dumps(combinations)}')")
|
|
```
|
|
|
|
## Behavior Details
|
|
|
|
### Click vs Right-Click Behavior
|
|
|
|
**IMPORTANT**: The library handles `click` and `right_click` differently:
|
|
|
|
**`click` (global detection)**:
|
|
- Triggers for ALL registered elements, regardless of where you click
|
|
- Useful for "click outside to close" functionality (modals, dropdowns, popups)
|
|
- Example: Modal registered with `click` → clicking anywhere on the page triggers the modal's click action
|
|
|
|
**`right_click` (element-specific detection)**:
|
|
- Triggers ONLY when you right-click on (or inside) the registered element
|
|
- Right-clicking outside the element does nothing and shows browser's context menu
|
|
- This preserves normal browser behavior while adding custom actions on your elements
|
|
|
|
**Example use case**:
|
|
```javascript
|
|
// Modal that closes when clicking anywhere
|
|
add_mouse_support('modal', JSON.stringify({
|
|
"click": {"hx-post": "/close-modal"} // Triggers even if you click outside modal
|
|
}));
|
|
|
|
// Context menu that only appears on element
|
|
add_mouse_support('item', JSON.stringify({
|
|
"right_click": {"hx-post": "/item-menu"} // Only triggers when right-clicking the item
|
|
}));
|
|
```
|
|
|
|
### Modifier Keys (Cross-Platform)
|
|
|
|
- **Windows/Linux**: `ctrl+click` uses Ctrl key
|
|
- **Mac**: `ctrl+click` uses Cmd (⌘) key OR Ctrl key
|
|
- This follows standard web conventions for cross-platform compatibility
|
|
|
|
### Input Context Protection
|
|
|
|
Mouse actions are **disabled** when clicking in input fields:
|
|
- `<input>` elements
|
|
- `<textarea>` elements
|
|
- Any `contenteditable` element
|
|
|
|
This ensures normal text selection and interaction works in forms.
|
|
|
|
### Right-Click Menu
|
|
|
|
The contextmenu (right-click menu) is prevented with `preventDefault()` when:
|
|
- A `right_click` action is configured for the element
|
|
- The element is NOT an input/textarea/contenteditable
|
|
|
|
### Event Bubbling
|
|
|
|
The library checks if the click target OR any parent element is registered:
|
|
```html
|
|
<div id="container">
|
|
<button>Click me</button>
|
|
</div>
|
|
```
|
|
If `container` is registered, clicking the button will trigger the container's actions.
|
|
|
|
## Examples
|
|
|
|
### Using Aliases
|
|
|
|
You can use `rclick` instead of `right_click` anywhere:
|
|
|
|
```javascript
|
|
// These are equivalent
|
|
const config1 = {
|
|
"right_click": {"hx-post": "/menu"}
|
|
};
|
|
|
|
const config2 = {
|
|
"rclick": {"hx-post": "/menu"} // Shorter alias
|
|
};
|
|
|
|
// Works in sequences too
|
|
const config3 = {
|
|
"click rclick": {"hx-post": "/sequence"}
|
|
};
|
|
|
|
// Works with modifiers
|
|
const config4 = {
|
|
"ctrl+rclick": {"hx-post": "/ctrl-right-click"}
|
|
};
|
|
```
|
|
|
|
### Context Menu
|
|
|
|
```javascript
|
|
const combinations = {
|
|
"right_click": {
|
|
"hx-post": "/show-context-menu",
|
|
"hx-target": "#menu",
|
|
"hx-swap": "innerHTML"
|
|
}
|
|
};
|
|
```
|
|
|
|
### Close Modal/Popup on Click Outside
|
|
|
|
Since `click` is detected globally (anywhere on the page), it's perfect for "click outside to close":
|
|
|
|
```javascript
|
|
// Modal element
|
|
const modalCombinations = {
|
|
"click": {
|
|
"hx-post": "/close-modal",
|
|
"hx-target": "#modal",
|
|
"hx-swap": "outerHTML"
|
|
}
|
|
};
|
|
add_mouse_support('modal', JSON.stringify(modalCombinations));
|
|
|
|
// Now clicking ANYWHERE on the page will trigger the handler
|
|
// Your backend receives:
|
|
// - clicked_inside: true (if clicked on modal) → maybe keep it open
|
|
// - clicked_inside: false (if clicked outside) → close it!
|
|
```
|
|
|
|
**Backend example (Python/Flask)**:
|
|
```python
|
|
@app.route('/close-modal', methods=['POST'])
|
|
def close_modal():
|
|
clicked_inside = request.form.get('clicked_inside') == 'true'
|
|
|
|
if clicked_inside:
|
|
# User clicked inside the modal - keep it open
|
|
return render_template('modal_content.html')
|
|
else:
|
|
# User clicked outside - close the modal
|
|
return '' # Empty response removes the modal
|
|
```
|
|
|
|
**Note**: The click handler on the modal element will trigger for all clicks on the page, not just clicks on the modal itself. Use the `clicked_inside` parameter to determine the appropriate action.
|
|
|
|
### Multi-Select List
|
|
|
|
```javascript
|
|
const combinations = {
|
|
"click": {
|
|
"hx-post": "/select-item",
|
|
"hx-vals": {"mode": "single"}
|
|
},
|
|
"ctrl+click": {
|
|
"hx-post": "/select-item",
|
|
"hx-vals": {"mode": "toggle"}
|
|
},
|
|
"shift+click": {
|
|
"hx-post": "/select-range",
|
|
"hx-vals": {"mode": "range"}
|
|
}
|
|
};
|
|
```
|
|
|
|
### Interactive Canvas/Drawing
|
|
|
|
```javascript
|
|
const combinations = {
|
|
"click": {
|
|
"hx-post": "/draw-point"
|
|
},
|
|
"ctrl+click": {
|
|
"hx-post": "/draw-special"
|
|
},
|
|
"click click": {
|
|
"hx-post": "/confirm-action"
|
|
}
|
|
};
|
|
```
|
|
|
|
### Drag-and-Drop Alternative
|
|
|
|
```javascript
|
|
const combinations = {
|
|
"click": {
|
|
"hx-post": "/select-source"
|
|
},
|
|
"click click": {
|
|
"hx-post": "/set-destination"
|
|
}
|
|
};
|
|
```
|
|
|
|
## Troubleshooting
|
|
|
|
### Clicks not detected
|
|
|
|
- Verify the element exists and has the correct ID
|
|
- Check browser console for errors
|
|
- Ensure HTMX is loaded before mouse_support.js
|
|
|
|
### Right-click menu still appears
|
|
|
|
- Check if element is in input context (input/textarea)
|
|
- Verify the combination is configured correctly
|
|
- Check browser console for configuration errors
|
|
|
|
### Sequences not working
|
|
|
|
- Ensure clicks happen within 500ms timeout
|
|
- Check if longer sequences exist (causes waiting)
|
|
- Verify the combination string format (space-separated)
|
|
|
|
## Technical Details
|
|
|
|
### Architecture
|
|
|
|
- **Global listeners** on `document` for `click` and `contextmenu` 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)
|
|
|
|
### Performance
|
|
|
|
- Single event listener regardless of number of elements
|
|
- O(n) matching where n is sequence length
|
|
- Efficient memory usage with automatic cleanup
|
|
|
|
### Browser Compatibility
|
|
|
|
- Modern browsers (ES6+ required)
|
|
- Chrome, Firefox, Safari, Edge
|
|
- Requires HTMX library
|
|
|
|
## Notes
|
|
|
|
- Timeout value is the same as keyboard support (500ms) but in separate variable
|
|
- Can be used independently or alongside keyboard support
|
|
- Does not interfere with normal mouse behavior in inputs
|
|
- Element must exist in DOM when `add_mouse_support()` is called
|
|
- **Alias**: `rclick` can be used interchangeably with `right_click` for shorter syntax |