# 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

**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
- `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 {};
}
```

## 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)

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,
        on_move: str = None)
```

**Parameters**:
- `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
- `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

**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()")
```

**With drag and real-time feedback**:
```python
mouse.add(
    "mousedown>mouseup",
    selection_command,
    hx_vals="js:getCellId()",
    on_move="js:highlightDragRange()"
)
```

### 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"
  }
};
```

### 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

- 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)

### 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`, `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

- 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