diff --git a/src/myfasthtml/assets/myfasthtml.css b/src/myfasthtml/assets/myfasthtml.css
index 4e64e87..74d2127 100644
--- a/src/myfasthtml/assets/myfasthtml.css
+++ b/src/myfasthtml/assets/myfasthtml.css
@@ -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);
+}
diff --git a/src/myfasthtml/assets/myfasthtml.js b/src/myfasthtml/assets/myfasthtml.js
index c697774..1c35ad2 100644
--- a/src/myfasthtml/assets/myfasthtml.js
+++ b/src/myfasthtml/assets/myfasthtml.js
@@ -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();
}
diff --git a/src/myfasthtml/controls/DataGrid.py b/src/myfasthtml/controls/DataGrid.py
index a2f6b44..e046948 100644
--- a/src/myfasthtml/controls/DataGrid.py
+++ b/src/myfasthtml/controls/DataGrid.py
@@ -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)
diff --git a/src/myfasthtml/controls/DataGridColumnsManager.py b/src/myfasthtml/controls/DataGridColumnsManager.py
index 4114093..636f2b0 100644
--- a/src/myfasthtml/controls/DataGridColumnsManager.py
+++ b/src/myfasthtml/controls/DataGridColumnsManager.py
@@ -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):
diff --git a/src/myfasthtml/controls/helpers.py b/src/myfasthtml/controls/helpers.py
index c831da6..790133d 100644
--- a/src/myfasthtml/controls/helpers.py
+++ b/src/myfasthtml/controls/helpers.py
@@ -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,
+}