Added mouse selection

This commit is contained in:
2026-02-09 23:46:31 +01:00
parent b0d565589a
commit 79c37493af
7 changed files with 659 additions and 11 deletions

View File

@@ -152,6 +152,13 @@ class Commands(BaseCommands):
self._owner.on_click
).htmx(target=f"#tsm_{self._id}")
def on_mouse_selection(self):
return Command("OnMouseSelection",
"Range selection with mouse",
self._owner,
self._owner.on_mouse_selection
).htmx(target=f"#tsm_{self._id}")
def toggle_columns_manager(self):
return Command("ToggleColumnsManager",
"Hide/Show Columns Manager",
@@ -231,6 +238,7 @@ class DataGrid(MultipleInstance):
# other definitions
self._mouse_support = {
"mousedown>mouseup": {"command": self.commands.on_mouse_selection(), "hx_vals": "js:getCellId()"},
"click": {"command": self.commands.on_click(), "hx_vals": "js:getCellId()"},
"ctrl+click": {"command": self.commands.on_click(), "hx_vals": "js:getCellId()"},
"shift+click": {"command": self.commands.on_click(), "hx_vals": "js:getCellId()"},
@@ -479,12 +487,30 @@ class DataGrid(MultipleInstance):
def on_click(self, combination, is_inside, cell_id):
logger.debug(f"on_click {combination=} {is_inside=} {cell_id=}")
if is_inside and cell_id:
self._state.selection.extra_selected.clear()
if cell_id.startswith("tcell_"):
pos = self._get_pos_from_element_id(cell_id)
self._update_current_position(pos)
return self.render_partial()
def on_mouse_selection(self, combination, is_inside, cell_id_mousedown, cell_id_mouseup):
logger.debug(f"on_mouse_selection {combination=} {is_inside=} {cell_id_mousedown=} {cell_id_mouseup=}")
if (is_inside and
cell_id_mousedown and cell_id_mouseup and
cell_id_mousedown.startswith("tcell_") and cell_id_mouseup.startswith("tcell_")):
pos_mouse_down = self._get_pos_from_element_id(cell_id_mousedown)
pos_mouse_up = self._get_pos_from_element_id(cell_id_mouseup)
min_col, max_col = min(pos_mouse_down[0], pos_mouse_up[0]), max(pos_mouse_down[0], pos_mouse_up[0])
min_row, max_row = min(pos_mouse_down[1], pos_mouse_up[1]), max(pos_mouse_down[1], pos_mouse_up[1])
self._state.selection.extra_selected.clear()
self._state.selection.extra_selected.append(("range", (min_col, min_row, max_col, max_row)))
return self.render_partial()
def on_column_changed(self):
logger.debug("on_column_changed")
return self.render_partial("table")
@@ -752,6 +778,9 @@ class DataGrid(MultipleInstance):
# selected.append(("cell", self._get_element_id_from_pos("cell", self._state.selection.selected)))
selected.append(("focus", self._get_element_id_from_pos("cell", self._state.selection.selected)))
for extra_sel in self._state.selection.extra_selected:
selected.append(extra_sel)
return Div(
*[Div(selection_type=s_type, element_id=f"{elt_id}") for s_type, elt_id in selected],
id=f"tsm_{self._id}",

View File

@@ -19,11 +19,62 @@ class Mouse(MultipleInstance):
- Both (named params override command): mouse.add("click", command, hx_target="#other")
For dynamic hx_vals, use "js:functionName()" to call a client-side function.
Supported base actions:
- ``click`` - Left mouse click (detected globally)
- ``right_click`` (or alias ``rclick``) - Right mouse click (detected on element only)
- ``mousedown>mouseup`` - Left mouse press-and-release (captures data at both phases)
- ``rmousedown>mouseup`` - Right mouse press-and-release
Modifiers can be combined with ``+``: ``ctrl+click``, ``shift+mousedown>mouseup``.
Sequences use space separation: ``click right_click``, ``click mousedown>mouseup``.
For ``mousedown>mouseup`` actions with ``hx_vals="js:functionName()"``, the JS function
is called at both mousedown and mouseup. Results are suffixed: ``key_mousedown`` and
``key_mouseup`` in the server request.
"""
VALID_ACTIONS = {
'click', 'right_click', 'rclick',
'mousedown>mouseup', 'rmousedown>mouseup'
}
VALID_MODIFIERS = {'ctrl', 'shift', 'alt'}
def __init__(self, parent, _id=None, combinations=None):
super().__init__(parent, _id=_id)
self.combinations = combinations or {}
def _validate_sequence(self, sequence: str):
"""
Validate a mouse event sequence string.
Checks that all elements in the sequence use valid action names and modifiers.
Args:
sequence: Mouse event sequence string (e.g., "click", "ctrl+mousedown>mouseup")
Raises:
ValueError: If any action or modifier is invalid.
"""
elements = sequence.strip().split()
for element in elements:
parts = element.split('+')
# Last part should be the action, others are modifiers
action = parts[-1].lower()
modifiers = [p.lower() for p in parts[:-1]]
if action not in self.VALID_ACTIONS:
raise ValueError(
f"Invalid action '{action}' in sequence '{sequence}'. "
f"Valid actions: {', '.join(sorted(self.VALID_ACTIONS))}"
)
for mod in modifiers:
if mod not in self.VALID_MODIFIERS:
raise ValueError(
f"Invalid modifier '{mod}' in sequence '{sequence}'. "
f"Valid modifiers: {', '.join(sorted(self.VALID_MODIFIERS))}"
)
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,
@@ -32,7 +83,11 @@ class Mouse(MultipleInstance):
Add a mouse combination with optional command and HTMX parameters.
Args:
sequence: Mouse event sequence (e.g., "click", "ctrl+click", "click right_click")
sequence: Mouse event sequence string. Supports:
- Simple actions: ``"click"``, ``"right_click"``, ``"mousedown>mouseup"``
- Modifiers: ``"ctrl+click"``, ``"shift+mousedown>mouseup"``
- Sequences: ``"click right_click"``, ``"click mousedown>mouseup"``
- Aliases: ``"rclick"`` for ``"right_click"``
command: Optional Command object for server-side action
hx_post: HTMX post URL (overrides command)
hx_get: HTMX get URL (overrides command)
@@ -41,11 +96,17 @@ class Mouse(MultipleInstance):
hx_patch: HTMX patch URL (overrides command)
hx_target: HTMX target selector (overrides command)
hx_swap: HTMX swap strategy (overrides command)
hx_vals: HTMX values dict or "js:functionName()" for dynamic values
hx_vals: HTMX values dict or "js:functionName()" for dynamic values.
For mousedown>mouseup actions, the JS function is called at both
mousedown and mouseup, with results suffixed ``_mousedown`` and ``_mouseup``.
Returns:
self for method chaining
Raises:
ValueError: If the sequence contains invalid actions or modifiers.
"""
self._validate_sequence(sequence)
self.combinations[sequence] = {
"command": command,
"hx_post": hx_post,

View File

@@ -30,10 +30,15 @@ class DatagridEditionState:
@dataclass
class DatagridSelectionState:
"""
element_id: str
"tcell_grid_id_col_row" for cell
(min_col, min_row, max_col, max_row) for range
"""
selected: tuple[int, int] | None = None # column first, then row
last_selected: tuple[int, int] | None = None
selection_mode: str = None # valid values are "row", "column" or None for "cell"
extra_selected: list[tuple[str, str | int]] = field(default_factory=list) # list(tuple(selection_mode, element_id))
selection_mode: str = None # valid values are "row", "column", "range" or None for "cell"
extra_selected: list[tuple[str, str | int | tuple]] = field(default_factory=list) # (selection_mode, element_id)
last_extra_selected: tuple[int, int] = None