I can edit a cell

This commit is contained in:
2026-03-16 21:16:21 +01:00
parent 0951680466
commit ef9f269a49
8 changed files with 182 additions and 24 deletions

View File

@@ -1,7 +1,6 @@
import html
import logging
import re
from dataclasses import dataclass
from functools import lru_cache
from typing import Optional
@@ -18,9 +17,7 @@ from myfasthtml.controls.Keyboard import Keyboard
from myfasthtml.controls.Mouse import Mouse
from myfasthtml.controls.Panel import Panel, PanelConf
from myfasthtml.controls.Query import Query, QUERY_FILTER
from myfasthtml.controls.datagrid_objects import DataGridColumnState, DataGridRowUiState, \
DatagridSelectionState, DataGridHeaderFooterConf, DatagridEditionState, DataGridColumnUiState, \
DataGridRowSelectionColumnState
from myfasthtml.controls.datagrid_objects import *
from myfasthtml.controls.helpers import mk
from myfasthtml.core.commands import Command
from myfasthtml.core.constants import ColumnType, FooterAggregation, DATAGRID_PAGE_SIZE, FILTER_INPUT_CID
@@ -156,7 +153,7 @@ class Commands(BaseCommands):
return Command("OnClick",
"Click on the table",
self._owner,
self._owner.on_click
self._owner.handle_on_click
).htmx(target=f"#tsm_{self._id}")
def on_key_pressed(self):
@@ -212,6 +209,21 @@ class Commands(BaseCommands):
self._owner,
self._owner.on_column_changed
)
def start_edition(self):
return Command("StartEdition",
"Enter cell edit mode",
self._owner,
self._owner.handle_start_edition
).htmx(target=f"#tsm_{self._id}")
def save_edition(self):
return Command("SaveEdition",
"Save cell edition",
self._owner,
self._owner.handle_save_edition
).htmx(target=f"#tsm_{self._id}",
trigger="blur, keydown[key=='Enter']")
class DataGrid(MultipleInstance):
@@ -293,10 +305,12 @@ class DataGrid(MultipleInstance):
"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()"},
"dblclick": {"command": self.commands.start_edition(), "hx_vals": "js:getCellId()"},
}
self._key_support = {
"esc": {"command": self.commands.on_key_pressed(), "require_inside": False},
"enter": {"command": self.commands.on_key_pressed(), "require_inside": True},
}
logger.debug(f"DataGrid '{self.get_table_name()}' with id='{self._id}' created.")
@@ -451,6 +465,26 @@ class DataGrid(MultipleInstance):
if self._settings.enable_edition:
self._columns.insert(0, DataGridRowSelectionColumnState())
def _enter_edition(self, pos):
col_pos, row_index = pos
col_def = self._columns[col_pos]
if col_def.type in (ColumnType.RowSelection_, ColumnType.RowIndex, ColumnType.Formula):
return self.render_partial()
self._state.edition.under_edition = pos
self._state.save()
return self.render_partial("cell", pos=pos)
def _convert_edition_value(self, value_str, col_type):
if col_type == ColumnType.Number:
try:
return float(value_str) if '.' in value_str else int(value_str)
except (ValueError, TypeError):
return value_str
elif col_type == ColumnType.Bool:
return value_str.lower() in ('true', '1', 'yes')
else:
return value_str
def add_new_column(self, col_def: DataGridColumnState) -> None:
"""Add a new column, delegating data mutation to DataService.
@@ -571,14 +605,20 @@ class DataGrid(MultipleInstance):
self._state.filtered[FILTER_INPUT_CID] = self._datagrid_filter.get_query()
return self.render_partial("body")
def on_click(self, combination, is_inside, cell_id):
def handle_on_click(self, combination, is_inside, cell_id):
logger.debug(f"on_click table={self.get_table_name()} {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)
pos = self._get_pos_from_element_id(cell_id)
if (self._settings.enable_edition and
pos is not None and
pos == self._state.selection.selected and
self._state.edition.under_edition is None):
return self._enter_edition(pos)
self._update_current_position(pos)
return self.render_partial()
@@ -605,6 +645,11 @@ class DataGrid(MultipleInstance):
if combination == "esc":
self._update_current_position(None)
self._state.selection.extra_selected.clear()
elif (combination == "enter" and
self._settings.enable_edition and
self._state.selection.selected and
self._state.edition.under_edition is None):
return self._enter_edition(self._state.selection.selected)
return self.render_partial()
@@ -674,6 +719,30 @@ class DataGrid(MultipleInstance):
self._panel.set_title(side="right", title="Formatting")
self._panel.set_right(self._formatting_editor)
def handle_start_edition(self, cell_id):
logger.debug(f"handle_start_edition: {cell_id=}")
if not self._settings.enable_edition:
return self.render_partial()
if self._state.edition.under_edition is not None:
return self.render_partial()
pos = self._get_pos_from_element_id(cell_id)
if pos is None:
return self.render_partial()
self._update_current_position(pos)
return self._enter_edition(pos)
def handle_save_edition(self, value):
logger.debug(f"handle_save_edition: {value=}")
if self._state.edition.under_edition is None:
return self.render_partial()
col_pos, row_index = self._state.edition.under_edition
col_def = self._columns[col_pos]
typed_value = self._convert_edition_value(value, col_def.type)
self._data_service.set_data(col_def.col_id, row_index, typed_value)
self._state.edition.under_edition = None
self._state.save()
return self.render_partial("cell", pos=(col_pos, row_index))
def handle_set_column_width(self, col_id: str, width: str):
"""Update column width after resize. Called via Command from JS."""
logger.debug(f"set_column_width: {col_id=} {width=}")
@@ -724,6 +793,31 @@ class DataGrid(MultipleInstance):
def get_data_service_id_from_data_grid_id(datagrid_id):
return datagrid_id.replace(DataGrid.compute_prefix(), DataService.compute_prefix(), 1)
def _mk_edition_cell(self, col_pos, row_index, col_def: DataGridColumnState, is_last):
col_array = self._fast_access.get(col_def.col_id)
value = col_array[row_index] if col_array is not None and row_index < len(col_array) else None
value_str = str(value) if not is_null(value) else ""
save_cmd = self.commands.save_edition()
input_elem = mk.mk(
Input(value=value_str, name="value", autofocus=True, cls="dt2-cell-input"),
command=save_cmd
)
return OptimizedDiv(
input_elem,
id=self._get_element_id_from_pos("cell", (col_pos, row_index)),
cls=merge_classes("dt2-cell dt2-cell-edition", "dt2-last-cell" if is_last else None),
style=f"width:{col_def.width}px;"
)
def _mk_cell_oob(self, col_pos, row_index):
col_def = self._columns[col_pos]
filter_keyword = self._state.filtered.get(FILTER_INPUT_CID)
filter_keyword_lower = filter_keyword.lower() if filter_keyword else None
is_last = col_pos == len(self._columns) - 1
return self.mk_body_cell(col_pos, row_index, col_def, filter_keyword_lower, is_last)
def mk_headers(self):
resize_cmd = self.commands.set_column_width()
move_cmd = self.commands.move_column()
@@ -867,6 +961,10 @@ class DataGrid(MultipleInstance):
if col_def.type == ColumnType.RowSelection_:
return OptimizedDiv(cls="dt2-row-selection")
if (self._settings.enable_edition and
self._state.edition.under_edition == (col_pos, row_index)):
return self._mk_edition_cell(col_pos, row_index, col_def, is_last)
col_array = self._fast_access.get(col_def.col_id)
value = col_array[row_index] if col_array is not None and row_index < len(col_array) else None
content = self.mk_body_cell_content(col_pos, row_index, col_def, filter_keyword_lower)
@@ -1105,7 +1203,7 @@ class DataGrid(MultipleInstance):
:param kwargs: Additional parameters for specific fragments (col_id, optimal_width for header)
:return:
"""
res = []
res = [self.mk_selection_manager()]
extra_attr = {
"hx-on::after-settle": f"initDataGrid('{self._id}');",
@@ -1133,7 +1231,21 @@ class DataGrid(MultipleInstance):
header.attrs.update(header_extra_attr)
return header
res.append(self.mk_selection_manager())
else:
col_pos, row_index = None, None
if (cell_id := kwargs.get("cell_id")) is not None:
col_pos, row_index = self._get_pos_from_element_id(cell_id)
elif (pos := kwargs.get("pos")) is not None:
col_pos, row_index = pos
if col_pos is not None and row_index is not None:
col_def = self._columns[col_pos]
filter_keyword = self._state.filtered.get(FILTER_INPUT_CID)
filter_keyword_lower = filter_keyword.lower() if filter_keyword else None
is_last_col = col_pos == len(self._columns) - 1
cell = self.mk_body_cell(col_pos, row_index, col_def, filter_keyword_lower, is_last_col)
res.append(cell)
return tuple(res)

View File

@@ -36,7 +36,8 @@ class Mouse(MultipleInstance):
VALID_ACTIONS = {
'click', 'right_click', 'rclick',
'mousedown>mouseup', 'rmousedown>mouseup'
'mousedown>mouseup', 'rmousedown>mouseup',
'dblclick', 'double_click', 'dclick'
}
VALID_MODIFIERS = {'ctrl', 'shift', 'alt'}
def __init__(self, parent, _id=None, combinations=None):