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, +}