Optimizing keyboard navigation and selection handling

This commit is contained in:
2026-03-21 18:08:13 +01:00
parent 853bc4abae
commit f887267362
5 changed files with 82 additions and 25 deletions

View File

@@ -1,3 +1,4 @@
import bisect
import html
import logging
import re
@@ -80,6 +81,7 @@ class DatagridState(DbObject):
self.selection: DatagridSelectionState = DatagridSelectionState()
self.cell_formats: dict = {}
self.table_format: list = []
self.ns_visible_indices: list[int] | None = None
class DatagridSettings(DbObject):
@@ -403,8 +405,12 @@ class DataGrid(MultipleInstance):
if col.visible and col.type != ColumnType.RowSelection_]
def _get_visible_row_indices(self) -> list[int]:
df = self._get_filtered_df()
return list(df.index) if df is not None else []
if self._state.ns_visible_indices is None:
if self._df is None:
self._state.ns_visible_indices = []
else:
self._state.ns_visible_indices = list(self._apply_filter(self._df).index)
return self._state.ns_visible_indices
def _navigate(self, pos: tuple, direction: str) -> tuple:
col_pos, row_index = pos
@@ -421,11 +427,11 @@ class DataGrid(MultipleInstance):
prev_cols = [c for c in navigable_cols if c < col_pos]
return (prev_cols[-1], row_index) if prev_cols else pos
elif direction == "down":
next_rows = [r for r in visible_rows if r > row_index]
return (col_pos, next_rows[0]) if next_rows else pos
next_pos = bisect.bisect_right(visible_rows, row_index)
return (col_pos, visible_rows[next_pos]) if next_pos < len(visible_rows) else pos
elif direction == "up":
prev_rows = [r for r in visible_rows if r < row_index]
return (col_pos, prev_rows[-1]) if prev_rows else pos
prev_pos = bisect.bisect_left(visible_rows, row_index) - 1
return (col_pos, visible_rows[prev_pos]) if prev_pos >= 0 else pos
return pos
@@ -439,9 +445,13 @@ class DataGrid(MultipleInstance):
return None
def _update_current_position(self, pos):
def _update_current_position(self, pos, reset_selection: bool = False):
self._state.selection.last_selected = self._state.selection.selected
self._state.selection.selected = pos
if reset_selection:
self._state.selection.extra_selected.clear()
self._state.edition.under_edition = None
self._state.save()
def _get_format_rules(self, col_pos, row_index, col_def):
@@ -508,14 +518,23 @@ class DataGrid(MultipleInstance):
self._columns.insert(0, DataGridRowSelectionColumnState())
def _enter_edition(self, pos):
logger.debug(f"enter_edition: {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()
if col_def.type == ColumnType.Bool:
return self._toggle_bool_cell(col_pos, row_index, col_def)
self._state.edition.under_edition = pos
self._state.save()
return self.render_partial("cell", pos=pos)
def _toggle_bool_cell(self, col_pos, row_index, col_def):
col_array = self._fast_access.get(col_def.col_id)
current_value = col_array[row_index] if col_array is not None and row_index < len(col_array) else False
self._data_service.set_data(col_def.col_id, row_index, not bool(current_value))
return self.render_partial("cell", pos=(col_pos, row_index))
def _convert_edition_value(self, value_str, col_type):
if col_type == ColumnType.Number:
try:
@@ -645,11 +664,13 @@ class DataGrid(MultipleInstance):
def filter(self):
logger.debug("filter")
self._state.filtered[FILTER_INPUT_CID] = self._datagrid_filter.get_query()
self._state.ns_visible_indices = None
return self.render_partial("body")
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:
logger.debug(f" is_inside=True")
self._state.selection.extra_selected.clear()
pos = self._get_pos_from_element_id(cell_id)
@@ -659,9 +680,15 @@ class DataGrid(MultipleInstance):
pos == self._state.selection.selected and
self._state.edition.under_edition is None):
return self._enter_edition(pos)
else:
logger.debug(
f" {pos=}, selected={self._state.selection.selected}, under_edition={self._state.edition.under_edition}")
self._update_current_position(pos)
else:
logger.debug(f" is_inside=False")
return self.render_partial()
def on_mouse_selection(self, combination, is_inside, cell_id_mousedown, cell_id_mouseup):
@@ -685,13 +712,15 @@ class DataGrid(MultipleInstance):
def on_key_pressed(self, combination, has_focus, is_inside):
logger.debug(f"on_key_pressed table={self.get_table_name()} {combination=} {has_focus=} {is_inside=}")
if combination == "esc":
self._update_current_position(None)
self._state.selection.extra_selected.clear()
self._update_current_position(None, reset_selection=True)
return self.render_partial("cell", pos=self._state.selection.last_selected)
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)
elif combination in self._ARROW_KEY_DIRECTIONS:
current_pos = (self._state.selection.selected
or self._state.selection.last_selected
@@ -1139,25 +1168,19 @@ class DataGrid(MultipleInstance):
)
def mk_selection_manager(self):
extra_attr = {
"hx-on::after-settle": f"updateDatagridSelection('{self._id}');",
}
selected = []
if self._state.selection.selected:
# 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}",
selection_mode=f"{self._state.selection.selection_mode}",
**extra_attr,
)
def mk_aggregation_cell(self, col_def, row_index: int, footer_conf, oob=False):
@@ -1244,7 +1267,8 @@ class DataGrid(MultipleInstance):
id=self._id,
cls="grid",
style="height: 100%; grid-template-rows: auto 1fr;",
tabindex="-1"
tabindex="-1",
**{"hx-on:htmx:after-swap": f"if(event.detail.target.id==='tsm_{self._id}') updateDatagridSelection('{self._id}');"}
)
def render_partial(self, fragment="cell", **kwargs):