I can hide and show the columns, and it dynamically updates the grid

This commit is contained in:
2026-01-25 18:48:54 +01:00
parent 3abfab8e97
commit e31d9026ce
5 changed files with 146 additions and 35 deletions

View File

@@ -1161,3 +1161,19 @@
.dt2-moving {
transition: transform 300ms ease;
}
/* *********************************************** */
/* ******** DataGrid Column Manager ********** */
/* *********************************************** */
.dt2-column-manager-label {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
border-radius: 0.375rem;
transition: background-color 0.15s ease;
}
.dt2-column-manager-label:hover {
background-color: var(--color-base-300);
}

View File

@@ -1200,8 +1200,10 @@ function triggerHtmxAction(elementId, config, combinationStr, isInside, event) {
}
}
// Prevent default if we found any match and not in input context
if (currentMatches.length > 0 && !isInInputContext()) {
// Prevent default only if click was INSIDE a registered element
// Clicks outside should preserve native behavior (checkboxes, buttons, etc.)
const anyMatchInside = currentMatches.some(match => match.isInside);
if (currentMatches.length > 0 && anyMatchInside && !isInInputContext()) {
event.preventDefault();
}

View File

@@ -17,7 +17,7 @@ from myfasthtml.controls.Mouse import Mouse
from myfasthtml.controls.Panel import Panel, PanelConf
from myfasthtml.controls.datagrid_objects import DataGridColumnState, DataGridRowState, \
DatagridSelectionState, DataGridHeaderFooterConf, DatagridEditionState
from myfasthtml.controls.helpers import mk
from myfasthtml.controls.helpers import mk, icons
from myfasthtml.core.commands import Command
from myfasthtml.core.constants import ColumnType, ROW_INDEX_ID, FooterAggregation, DATAGRID_PAGE_SIZE, FILTER_INPUT_CID
from myfasthtml.core.dbmanager import DbObject
@@ -137,6 +137,13 @@ class Commands(BaseCommands):
self._owner,
self._owner.toggle_columns_manager
).htmx(target=None)
def on_column_changed(self):
return Command("OnColumnChanged",
"Column definition changed",
self._owner,
self._owner.on_column_changed
)
class DataGrid(MultipleInstance):
@@ -170,6 +177,7 @@ class DataGrid(MultipleInstance):
# add columns manager
self._columns_manager = DataGridColumnsManager(self)
self._columns_manager.bind_command("ToggleColumn", self.commands.on_column_changed())
# other definitions
self._mouse_support = {
@@ -354,7 +362,7 @@ class DataGrid(MultipleInstance):
def filter(self):
logger.debug("filter")
self._state.filtered[FILTER_INPUT_CID] = self._datagrid_filter.get_query()
return self.render_partial("body", redraw_scrollbars=True)
return self.render_partial("body")
def on_click(self, combination, is_inside, cell_id):
logger.debug(f"on_click {combination=} {is_inside=} {cell_id=}")
@@ -365,6 +373,10 @@ class DataGrid(MultipleInstance):
return self.render_partial()
def on_column_changed(self):
logger.debug("on_column_changed")
return self.render_partial("table")
def change_selection_mode(self):
logger.debug(f"change_selection_mode")
new_state = self._selection_mode_selector.get_state()
@@ -377,17 +389,27 @@ class DataGrid(MultipleInstance):
logger.debug(f"toggle_columns_manager")
self._panel.set_right(self._columns_manager)
def save_state(self):
self._state.save()
def get_state(self):
return self._state
def mk_headers(self):
resize_cmd = self.commands.set_column_width()
move_cmd = self.commands.move_column()
def _mk_header_name(col_def: DataGridColumnState):
return Div(
mk.label(col_def.title, name="dt2-header-title"),
mk.label(col_def.title, icon=icons.get(col_def.type, None)),
# make room for sort and filter indicators
cls="flex truncate cursor-default",
)
def _mk_header(col_def: DataGridColumnState):
if not col_def.visible:
return None
return Div(
_mk_header_name(col_def),
Div(cls="dt2-resize-handle", data_command_id=resize_cmd.id),
@@ -397,7 +419,7 @@ class DataGrid(MultipleInstance):
cls="dt2-cell dt2-resizable flex",
)
header_class = "dt2-row dt2-header" + "" if self._settings.header_visible else " hidden"
header_class = "dt2-row dt2-header"
return Div(
*[_mk_header(col_def) for col_def in self._state.columns],
cls=header_class,
@@ -470,7 +492,7 @@ class DataGrid(MultipleInstance):
return None
if not col_def.visible:
return OptimizedDiv(cls="dt2-col-hidden")
return None
value = self._state.ns_fast_access[col_def.col_id][row_index]
content = self.mk_body_cell_content(col_pos, row_index, col_def, filter_keyword_lower)
@@ -509,7 +531,7 @@ class DataGrid(MultipleInstance):
return rows
def mk_body_container(self):
def mk_body_wrapper(self):
return Div(
self.mk_body(),
cls="dt2-body-container",
@@ -535,28 +557,12 @@ class DataGrid(MultipleInstance):
id=f"tf_{self._id}"
)
def mk_table(self):
def mk_table_wrapper(self):
return Div(
self.mk_selection_manager(),
# Grid table with header, body, footer
Div(
# Header container - no scroll
Div(
self.mk_headers(),
cls="dt2-header-container"
),
self.mk_body_container(), # Body container - scroll via JS, scrollbars hidden
# Footer container - no scroll
Div(
self.mk_footers(),
cls="dt2-footer-container"
),
cls="dt2-table",
id=f"t_{self._id}"
),
self.mk_table(),
# Custom scrollbars overlaid
Div(
# Vertical scrollbar wrapper (right side)
@@ -575,6 +581,26 @@ class DataGrid(MultipleInstance):
id=f"tw_{self._id}"
)
def mk_table(self):
# Grid table with header, body, footer
return Div(
# Header container - no scroll
Div(
self.mk_headers(),
cls="dt2-header-container"
),
self.mk_body_wrapper(), # Body container - scroll via JS, scrollbars hidden
# Footer container - no scroll
Div(
self.mk_footers(),
cls="dt2-footer-container"
),
cls="dt2-table",
id=f"t_{self._id}"
)
def mk_selection_manager(self):
extra_attr = {
@@ -667,7 +693,7 @@ class DataGrid(MultipleInstance):
mk.icon(settings16_regular, command=self.commands.toggle_columns_manager(), tooltip="Show sidebar"),
cls="flex"),
cls="flex items-center justify-between mb-2"),
self._panel.set_main(self.mk_table()),
self._panel.set_main(self.mk_table_wrapper()),
Script(f"initDataGrid('{self._id}');"),
Mouse(self, combinations=self._mouse_support),
id=self._id,
@@ -675,7 +701,7 @@ class DataGrid(MultipleInstance):
style="height: 100%; grid-template-rows: auto 1fr;"
)
def render_partial(self, fragment="cell", redraw_scrollbars=False):
def render_partial(self, fragment="cell"):
"""
:param fragment: cell | body
@@ -689,10 +715,15 @@ class DataGrid(MultipleInstance):
}
if fragment == "body":
body_container = self.mk_body_container()
body_container = self.mk_body_wrapper()
body_container.attrs.update(extra_attr)
res.append(body_container)
elif fragment == "table":
table = self.mk_table()
table.attrs.update(extra_attr)
res.append(table)
res.append(self.mk_selection_manager())
return tuple(res)

View File

@@ -1,23 +1,61 @@
import logging
from fasthtml.components import *
from myfasthtml.controls.BaseCommands import BaseCommands
from myfasthtml.controls.Search import Search
from myfasthtml.controls.datagrid_objects import DataGridColumnState
from myfasthtml.controls.helpers import icons, mk
from myfasthtml.core.commands import Command
from myfasthtml.core.instances import MultipleInstance
from myfasthtml.icons.fluent_p1 import chevron_right20_regular
logger = logging.getLogger("DataGridColumnsManager")
class Commands(BaseCommands):
def toggle_column(self, col_id):
return Command(f"ToggleColumn",
f"Toggle column {col_id}",
self._owner,
self._owner.toggle_column,
kwargs={"col_id": col_id}).htmx(swap="outerHTML", target=f"#tcolman_{self._id}-{col_id}")
class DataGridColumnsManager(MultipleInstance):
def __init__(self, parent, _id=None):
super().__init__(parent, _id=_id)
self.commands = Commands(self)
@property
def columns(self):
return self._parent._state.columns
return self._parent.get_state().columns
def toggle_column(self, col_id):
logger.debug(f"toggle_column {col_id=}")
cols_defs = [c for c in self.columns if c.col_id == col_id]
if not cols_defs:
logger.debug(f" column '{col_id}' is not found.")
return Div(f"Column '{col_id}' not found")
col_def = cols_defs[0]
col_def.visible = not col_def.visible
self._parent.save_state()
return self.mk_column(col_def)
def mk_column(self, col_def: DataGridColumnState):
return Div(
Input(type="checkbox", checked=col_def.visible, cls="ml-2"),
Label(col_def.col_id, cls="ml-2"),
cls="flex mb-1",
mk.mk(
Input(type="checkbox", cls="checkbox checkbox-sm", checked=col_def.visible),
command=self.commands.toggle_column(col_def.col_id)
),
Div(
Div(mk.label(col_def.col_id, icon=icons.get(col_def.type, None), cls="ml-2")),
Div(mk.icon(chevron_right20_regular), cls="mr-2"),
cls="dt2-column-manager-label"
),
cls="flex mb-1 items-center",
id=f"tcolman_{self._id}-{col_def.col_id}"
)
def render(self):

View File

@@ -1,8 +1,16 @@
from fastcore.xml import FT
from fasthtml.components import *
from myfasthtml.core.bindings import Binding
from myfasthtml.core.commands import Command, CommandTemplate
from myfasthtml.core.constants import ColumnType
from myfasthtml.core.utils import merge_classes
from myfasthtml.icons.fluent import question20_regular, brain_circuit20_regular, number_row20_regular, \
number_symbol20_regular
from myfasthtml.icons.fluent_p1 import checkbox_checked20_regular, checkbox_unchecked20_regular, \
checkbox_checked20_filled
from myfasthtml.icons.fluent_p2 import text_bullet_list_square20_regular, text_field20_regular
from myfasthtml.icons.fluent_p3 import calendar_ltr20_regular
class Ids:
@@ -96,7 +104,7 @@ class mk:
command: Command | CommandTemplate = None,
binding: Binding = None,
**kwargs):
merged_cls = merge_classes("flex truncate", cls, kwargs)
merged_cls = merge_classes("flex truncate items-center", cls, kwargs)
icon_part = Span(icon, cls=f"mf-icon-{mk.convert_size(size)} mr-1") if icon else None
text_part = Span(text, cls=f"text-{size}")
return mk.mk(Label(icon_part, text_part, cls=merged_cls, **kwargs), command=command, binding=binding)
@@ -138,3 +146,19 @@ class mk:
ft = mk.manage_command(ft, command) if command else ft
ft = mk.manage_binding(ft, binding, init_binding=init_binding) if binding else ft
return ft
icons = {
None: question20_regular,
True: checkbox_checked20_regular,
False: checkbox_unchecked20_regular,
"Brain": brain_circuit20_regular,
ColumnType.RowIndex: number_symbol20_regular,
ColumnType.Text: text_field20_regular,
ColumnType.Number: number_row20_regular,
ColumnType.Datetime: calendar_ltr20_regular,
ColumnType.Bool: checkbox_checked20_filled,
ColumnType.List: text_bullet_list_square20_regular,
}