I can hide and show the columns, and it dynamically updates the grid
This commit is contained in:
@@ -1161,3 +1161,19 @@
|
|||||||
.dt2-moving {
|
.dt2-moving {
|
||||||
transition: transform 300ms ease;
|
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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1200,8 +1200,10 @@ function triggerHtmxAction(elementId, config, combinationStr, isInside, event) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent default if we found any match and not in input context
|
// Prevent default only if click was INSIDE a registered element
|
||||||
if (currentMatches.length > 0 && !isInInputContext()) {
|
// Clicks outside should preserve native behavior (checkboxes, buttons, etc.)
|
||||||
|
const anyMatchInside = currentMatches.some(match => match.isInside);
|
||||||
|
if (currentMatches.length > 0 && anyMatchInside && !isInInputContext()) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ from myfasthtml.controls.Mouse import Mouse
|
|||||||
from myfasthtml.controls.Panel import Panel, PanelConf
|
from myfasthtml.controls.Panel import Panel, PanelConf
|
||||||
from myfasthtml.controls.datagrid_objects import DataGridColumnState, DataGridRowState, \
|
from myfasthtml.controls.datagrid_objects import DataGridColumnState, DataGridRowState, \
|
||||||
DatagridSelectionState, DataGridHeaderFooterConf, DatagridEditionState
|
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.commands import Command
|
||||||
from myfasthtml.core.constants import ColumnType, ROW_INDEX_ID, FooterAggregation, DATAGRID_PAGE_SIZE, FILTER_INPUT_CID
|
from myfasthtml.core.constants import ColumnType, ROW_INDEX_ID, FooterAggregation, DATAGRID_PAGE_SIZE, FILTER_INPUT_CID
|
||||||
from myfasthtml.core.dbmanager import DbObject
|
from myfasthtml.core.dbmanager import DbObject
|
||||||
@@ -138,6 +138,13 @@ class Commands(BaseCommands):
|
|||||||
self._owner.toggle_columns_manager
|
self._owner.toggle_columns_manager
|
||||||
).htmx(target=None)
|
).htmx(target=None)
|
||||||
|
|
||||||
|
def on_column_changed(self):
|
||||||
|
return Command("OnColumnChanged",
|
||||||
|
"Column definition changed",
|
||||||
|
self._owner,
|
||||||
|
self._owner.on_column_changed
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class DataGrid(MultipleInstance):
|
class DataGrid(MultipleInstance):
|
||||||
def __init__(self, parent, settings=None, save_state=None, _id=None):
|
def __init__(self, parent, settings=None, save_state=None, _id=None):
|
||||||
@@ -170,6 +177,7 @@ class DataGrid(MultipleInstance):
|
|||||||
|
|
||||||
# add columns manager
|
# add columns manager
|
||||||
self._columns_manager = DataGridColumnsManager(self)
|
self._columns_manager = DataGridColumnsManager(self)
|
||||||
|
self._columns_manager.bind_command("ToggleColumn", self.commands.on_column_changed())
|
||||||
|
|
||||||
# other definitions
|
# other definitions
|
||||||
self._mouse_support = {
|
self._mouse_support = {
|
||||||
@@ -354,7 +362,7 @@ class DataGrid(MultipleInstance):
|
|||||||
def filter(self):
|
def filter(self):
|
||||||
logger.debug("filter")
|
logger.debug("filter")
|
||||||
self._state.filtered[FILTER_INPUT_CID] = self._datagrid_filter.get_query()
|
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):
|
def on_click(self, combination, is_inside, cell_id):
|
||||||
logger.debug(f"on_click {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()
|
return self.render_partial()
|
||||||
|
|
||||||
|
def on_column_changed(self):
|
||||||
|
logger.debug("on_column_changed")
|
||||||
|
return self.render_partial("table")
|
||||||
|
|
||||||
def change_selection_mode(self):
|
def change_selection_mode(self):
|
||||||
logger.debug(f"change_selection_mode")
|
logger.debug(f"change_selection_mode")
|
||||||
new_state = self._selection_mode_selector.get_state()
|
new_state = self._selection_mode_selector.get_state()
|
||||||
@@ -377,17 +389,27 @@ class DataGrid(MultipleInstance):
|
|||||||
logger.debug(f"toggle_columns_manager")
|
logger.debug(f"toggle_columns_manager")
|
||||||
self._panel.set_right(self._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):
|
def mk_headers(self):
|
||||||
resize_cmd = self.commands.set_column_width()
|
resize_cmd = self.commands.set_column_width()
|
||||||
move_cmd = self.commands.move_column()
|
move_cmd = self.commands.move_column()
|
||||||
|
|
||||||
def _mk_header_name(col_def: DataGridColumnState):
|
def _mk_header_name(col_def: DataGridColumnState):
|
||||||
return Div(
|
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",
|
cls="flex truncate cursor-default",
|
||||||
)
|
)
|
||||||
|
|
||||||
def _mk_header(col_def: DataGridColumnState):
|
def _mk_header(col_def: DataGridColumnState):
|
||||||
|
if not col_def.visible:
|
||||||
|
return None
|
||||||
|
|
||||||
return Div(
|
return Div(
|
||||||
_mk_header_name(col_def),
|
_mk_header_name(col_def),
|
||||||
Div(cls="dt2-resize-handle", data_command_id=resize_cmd.id),
|
Div(cls="dt2-resize-handle", data_command_id=resize_cmd.id),
|
||||||
@@ -397,7 +419,7 @@ class DataGrid(MultipleInstance):
|
|||||||
cls="dt2-cell dt2-resizable flex",
|
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(
|
return Div(
|
||||||
*[_mk_header(col_def) for col_def in self._state.columns],
|
*[_mk_header(col_def) for col_def in self._state.columns],
|
||||||
cls=header_class,
|
cls=header_class,
|
||||||
@@ -470,7 +492,7 @@ class DataGrid(MultipleInstance):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
if not col_def.visible:
|
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]
|
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)
|
content = self.mk_body_cell_content(col_pos, row_index, col_def, filter_keyword_lower)
|
||||||
@@ -509,7 +531,7 @@ class DataGrid(MultipleInstance):
|
|||||||
|
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
def mk_body_container(self):
|
def mk_body_wrapper(self):
|
||||||
return Div(
|
return Div(
|
||||||
self.mk_body(),
|
self.mk_body(),
|
||||||
cls="dt2-body-container",
|
cls="dt2-body-container",
|
||||||
@@ -535,28 +557,12 @@ class DataGrid(MultipleInstance):
|
|||||||
id=f"tf_{self._id}"
|
id=f"tf_{self._id}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def mk_table(self):
|
def mk_table_wrapper(self):
|
||||||
return Div(
|
return Div(
|
||||||
self.mk_selection_manager(),
|
self.mk_selection_manager(),
|
||||||
|
|
||||||
# Grid table with header, body, footer
|
self.mk_table(),
|
||||||
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}"
|
|
||||||
),
|
|
||||||
# Custom scrollbars overlaid
|
# Custom scrollbars overlaid
|
||||||
Div(
|
Div(
|
||||||
# Vertical scrollbar wrapper (right side)
|
# Vertical scrollbar wrapper (right side)
|
||||||
@@ -575,6 +581,26 @@ class DataGrid(MultipleInstance):
|
|||||||
id=f"tw_{self._id}"
|
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):
|
def mk_selection_manager(self):
|
||||||
|
|
||||||
extra_attr = {
|
extra_attr = {
|
||||||
@@ -667,7 +693,7 @@ class DataGrid(MultipleInstance):
|
|||||||
mk.icon(settings16_regular, command=self.commands.toggle_columns_manager(), tooltip="Show sidebar"),
|
mk.icon(settings16_regular, command=self.commands.toggle_columns_manager(), tooltip="Show sidebar"),
|
||||||
cls="flex"),
|
cls="flex"),
|
||||||
cls="flex items-center justify-between mb-2"),
|
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}');"),
|
Script(f"initDataGrid('{self._id}');"),
|
||||||
Mouse(self, combinations=self._mouse_support),
|
Mouse(self, combinations=self._mouse_support),
|
||||||
id=self._id,
|
id=self._id,
|
||||||
@@ -675,7 +701,7 @@ class DataGrid(MultipleInstance):
|
|||||||
style="height: 100%; grid-template-rows: auto 1fr;"
|
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
|
:param fragment: cell | body
|
||||||
@@ -689,10 +715,15 @@ class DataGrid(MultipleInstance):
|
|||||||
}
|
}
|
||||||
|
|
||||||
if fragment == "body":
|
if fragment == "body":
|
||||||
body_container = self.mk_body_container()
|
body_container = self.mk_body_wrapper()
|
||||||
body_container.attrs.update(extra_attr)
|
body_container.attrs.update(extra_attr)
|
||||||
res.append(body_container)
|
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())
|
res.append(self.mk_selection_manager())
|
||||||
|
|
||||||
return tuple(res)
|
return tuple(res)
|
||||||
|
|||||||
@@ -1,23 +1,61 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
from fasthtml.components import *
|
from fasthtml.components import *
|
||||||
|
|
||||||
|
from myfasthtml.controls.BaseCommands import BaseCommands
|
||||||
from myfasthtml.controls.Search import Search
|
from myfasthtml.controls.Search import Search
|
||||||
from myfasthtml.controls.datagrid_objects import DataGridColumnState
|
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.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):
|
class DataGridColumnsManager(MultipleInstance):
|
||||||
def __init__(self, parent, _id=None):
|
def __init__(self, parent, _id=None):
|
||||||
super().__init__(parent, _id=_id)
|
super().__init__(parent, _id=_id)
|
||||||
|
self.commands = Commands(self)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def columns(self):
|
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):
|
def mk_column(self, col_def: DataGridColumnState):
|
||||||
return Div(
|
return Div(
|
||||||
Input(type="checkbox", checked=col_def.visible, cls="ml-2"),
|
mk.mk(
|
||||||
Label(col_def.col_id, cls="ml-2"),
|
Input(type="checkbox", cls="checkbox checkbox-sm", checked=col_def.visible),
|
||||||
cls="flex mb-1",
|
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):
|
def render(self):
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
|
from fastcore.xml import FT
|
||||||
from fasthtml.components import *
|
from fasthtml.components import *
|
||||||
|
|
||||||
from myfasthtml.core.bindings import Binding
|
from myfasthtml.core.bindings import Binding
|
||||||
from myfasthtml.core.commands import Command, CommandTemplate
|
from myfasthtml.core.commands import Command, CommandTemplate
|
||||||
|
from myfasthtml.core.constants import ColumnType
|
||||||
from myfasthtml.core.utils import merge_classes
|
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:
|
class Ids:
|
||||||
@@ -96,7 +104,7 @@ class mk:
|
|||||||
command: Command | CommandTemplate = None,
|
command: Command | CommandTemplate = None,
|
||||||
binding: Binding = None,
|
binding: Binding = None,
|
||||||
**kwargs):
|
**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
|
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}")
|
text_part = Span(text, cls=f"text-{size}")
|
||||||
return mk.mk(Label(icon_part, text_part, cls=merged_cls, **kwargs), command=command, binding=binding)
|
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_command(ft, command) if command else ft
|
||||||
ft = mk.manage_binding(ft, binding, init_binding=init_binding) if binding else ft
|
ft = mk.manage_binding(ft, binding, init_binding=init_binding) if binding else ft
|
||||||
return 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,
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user