Working on ColumnsManager. Added CycleStateControl and DataGridColumnsManager.

This commit is contained in:
2026-01-24 23:55:44 +01:00
parent 3c2c07ebfc
commit 0bd56c7f09
9 changed files with 1080 additions and 103 deletions

View File

@@ -0,0 +1,52 @@
from fasthtml.components import Div
from myfasthtml.controls.BaseCommands import BaseCommands
from myfasthtml.controls.helpers import mk
from myfasthtml.core.commands import Command
from myfasthtml.core.dbmanager import DbObject
from myfasthtml.core.instances import MultipleInstance
class CycleState(DbObject):
def __init__(self, owner, save_state):
with self.initializing():
super().__init__(owner, save_state=save_state)
self.state = None
class Commands(BaseCommands):
def cycle_state(self):
return Command("CycleState",
"Cycle state",
self._owner,
self._owner.cycle_state).htmx(target=f"#{self._id}")
class CycleStateControl(MultipleInstance):
def __init__(self, parent, controls: dict, _id=None, save_state=True):
super().__init__(parent, _id)
self._state = CycleState(self, save_state)
self.controls_by_states = controls
self.commands = Commands(self)
# init the state if required
if self._state.state is None and controls:
self._state.state = next(iter(controls.keys()))
def cycle_state(self):
keys = list(self.controls_by_states.keys())
current_idx = keys.index(self._state.state)
self._state.state = keys[(current_idx + 1) % len(keys)]
return self
def get_state(self):
return self._state.state
def render(self):
return mk.mk(
Div(self.controls_by_states[self._state.state], id=self._id),
command=self.commands.cycle_state()
)
def __ft__(self):
return self.render()

View File

@@ -10,6 +10,8 @@ from fasthtml.components import *
from pandas import DataFrame
from myfasthtml.controls.BaseCommands import BaseCommands
from myfasthtml.controls.CycleStateControl import CycleStateControl
from myfasthtml.controls.DataGridColumnsManager import DataGridColumnsManager
from myfasthtml.controls.DataGridQuery import DataGridQuery, DG_QUERY_FILTER
from myfasthtml.controls.Mouse import Mouse
from myfasthtml.controls.datagrid_objects import DataGridColumnState, DataGridRowState, \
@@ -21,6 +23,7 @@ from myfasthtml.core.dbmanager import DbObject
from myfasthtml.core.instances import MultipleInstance
from myfasthtml.core.optimized_ft import OptimizedDiv
from myfasthtml.core.utils import make_safe_id
from myfasthtml.icons.carbon import row, column, grid
from myfasthtml.icons.fluent import checkbox_unchecked16_regular
from myfasthtml.icons.fluent_p2 import checkbox_checked16_regular
@@ -112,6 +115,13 @@ class Commands(BaseCommands):
self._owner.filter
)
def change_selection_mode(self):
return Command("ChangeSelectionMode",
"Change selection mode",
self._owner,
self._owner.change_selection_mode
)
def on_click(self):
return Command("OnClick",
"Click on the table",
@@ -127,13 +137,32 @@ class DataGrid(MultipleInstance):
self._state = DatagridState(self, save_state=self._settings.save_state)
self.commands = Commands(self)
self.init_from_dataframe(self._state.ne_df, init_state=False) # state comes from DatagridState
# add DataGridQuery
self._datagrid_filter = DataGridQuery(self)
self._datagrid_filter.bind_command("QueryChanged", self.commands.filter())
self._datagrid_filter.bind_command("CancelQuery", self.commands.filter())
self._datagrid_filter.bind_command("ChangeFilterType", self.commands.filter())
# update the filter
self._state.filtered[FILTER_INPUT_CID] = self._datagrid_filter.get_query()
# add Selection Selector
selection_types = {
"row": mk.icon(row, tooltip="Row selection"),
"column": mk.icon(column, tooltip="Column selection"),
"cell": mk.icon(grid, tooltip="Cell selection")
}
self._selection_mode_selector = CycleStateControl(self, controls=selection_types, save_state=False)
self._selection_mode_selector.bind_command("CycleState", self.commands.change_selection_mode())
# add columns manager
self._columns_manager = DataGridColumnsManager(self)
# other definitions
self._mouse_support = {
"click": {"command": self.commands.on_click(), "hx_vals": "js:getCellId()"},
"ctrl+click": {"command": self.commands.on_click(), "hx_vals": "js:getCellId()"},
"shift+click": {"command": self.commands.on_click(), "hx_vals": "js:getCellId()"},
}
@property
def _df(self):
@@ -322,6 +351,14 @@ class DataGrid(MultipleInstance):
return self.render_partial()
def change_selection_mode(self):
logger.debug(f"change_selection_mode")
new_state = self._selection_mode_selector.get_state()
logger.debug(f" {new_state=}")
self._state.selection.selection_mode = new_state
self._state.save()
return self.render_partial()
def mk_headers(self):
resize_cmd = self.commands.set_column_width()
move_cmd = self.commands.move_column()
@@ -535,8 +572,7 @@ class DataGrid(MultipleInstance):
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}",
selection_mode=f"column",
selection_mode=f"{self._state.selection.selection_mode}",
**extra_attr,
)
@@ -606,17 +642,16 @@ class DataGrid(MultipleInstance):
if self._state.ne_df is None:
return Div("No data to display !")
mouse_support = {
"click": {"command": self.commands.on_click(), "hx_vals": "js:getCellId()"},
"ctrl+click": {"command": self.commands.on_click(), "hx_vals": "js:getCellId()"},
"shift+click": {"command": self.commands.on_click(), "hx_vals": "js:getCellId()"},
}
return Div(
Div(self._datagrid_filter, cls="mb-2"),
Div(self._datagrid_filter,
Div(
self._selection_mode_selector,
self._columns_manager,
cls="flex"),
cls="flex items-center justify-between mb-2"),
self.mk_table(),
Script(f"initDataGrid('{self._id}');"),
Mouse(self, combinations=mouse_support),
Mouse(self, combinations=self._mouse_support),
id=self._id,
cls="grid",
style="height: 100%; grid-template-rows: auto 1fr;"

View File

@@ -0,0 +1,15 @@
from fasthtml.components import Div
from myfasthtml.controls.Dropdown import Dropdown
from myfasthtml.controls.helpers import mk
from myfasthtml.icons.fluent_p1 import settings16_regular
class DataGridColumnsManager(Dropdown):
def __init__(self, parent, _id=None):
super().__init__(parent, _id=_id, align="right")
self.button = mk.icon(settings16_regular)
self.content = Div("DataGridColumnsManager")
def columns(self):
return self._parent._state.columns

View File

@@ -29,18 +29,43 @@ class DropdownState:
class Dropdown(MultipleInstance):
"""
Represents a dropdown component that can be toggled open or closed. This class is used
to create interactive dropdown elements, allowing for container and button customization.
The dropdown provides functionality to manage its state, including opening, closing, and
handling user interactions.
Interactive dropdown component that toggles open/closed on button click.
Provides automatic close behavior when clicking outside or pressing ESC.
Supports configurable positioning relative to the trigger button.
Args:
parent: Parent instance (required).
content: Content to display in the dropdown panel.
button: Trigger element that toggles the dropdown.
_id: Custom ID for the instance.
position: Vertical position relative to button.
- "below" (default): Dropdown appears below the button.
- "above": Dropdown appears above the button.
align: Horizontal alignment relative to button.
- "left" (default): Aligns to the left edge of the button.
- "right": Aligns to the right edge of the button.
- "center": Centers relative to the button.
Example:
dropdown = Dropdown(
parent=root,
button=Button("Menu"),
content=Ul(Li("Option 1"), Li("Option 2")),
position="below",
align="right"
)
"""
def __init__(self, parent, content=None, button=None, _id=None):
def __init__(self, parent, content=None, button=None, _id=None,
position="below", align="left"):
super().__init__(parent, _id=_id)
self.button = Div(button) if not isinstance(button, FT) else button
self.content = content
self.commands = Commands(self)
self._state = DropdownState()
self._position = position
self._align = align
def toggle(self):
self._state.opened = not self._state.opened
@@ -50,57 +75,32 @@ class Dropdown(MultipleInstance):
self._state.opened = False
return self._mk_content()
def on_click(self, combination, is_inside: bool):
def on_click(self, combination, is_inside: bool, is_button: bool = False):
if combination == "click":
self._state.opened = is_inside
if is_button:
self._state.opened = not self._state.opened
else:
self._state.opened = is_inside
return self._mk_content()
def _mk_content(self):
position_cls = f"mf-dropdown-{self._position}"
align_cls = f"mf-dropdown-{self._align}"
return Div(self.content,
cls=f"mf-dropdown {'is-visible' if self._state.opened else ''}",
cls=f"mf-dropdown {position_cls} {align_cls} {'is-visible' if self._state.opened else ''}",
id=f"{self._id}-content"),
def render(self):
return Div(
Div(
Div(self.button) if self.button else Div("None"),
Div(self.button if self.button else "None", cls="mf-dropdown-btn"),
self._mk_content(),
cls="mf-dropdown-wrapper"
),
Keyboard(self, _id="-keyboard").add("esc", self.commands.close()),
Mouse(self, "-mouse").add("click", self.commands.click()),
Mouse(self, "-mouse").add("click", self.commands.click(), hx_vals="js:getDropdownExtra()"),
id=self._id
)
def __ft__(self):
return self.render()
# document.addEventListener('htmx:afterSwap', function(event) {
# const targetElement = event.detail.target; // L'élément qui a été mis à jour (#popup-unique-id)
#
# // Vérifie si c'est bien notre popup
# if (targetElement.classList.contains('mf-popup-container')) {
#
# // Trouver l'élément déclencheur HTMX (le bouton existant)
# // HTMX stocke l'élément déclencheur dans event.detail.elt
# const trigger = document.querySelector('#mon-bouton-existant');
#
# if (trigger) {
# // Obtenir les coordonnées de l'élément déclencheur par rapport à la fenêtre
# const rect = trigger.getBoundingClientRect();
#
# // L'élément du popup à positionner
# const popup = targetElement;
#
# // Appliquer la position au conteneur du popup
# // On utilise window.scrollY pour s'assurer que la position est absolue par rapport au document,
# // et non seulement à la fenêtre (car le popup est en position: absolute, pas fixed)
#
# // Top: Juste en dessous de l'élément déclencheur
# popup.style.top = (rect.bottom + window.scrollY) + 'px';
#
# // Left: Aligner avec le côté gauche de l'élément déclencheur
# popup.style.left = (rect.left + window.scrollX) + 'px';
# }
# }
# });

View File

@@ -78,6 +78,7 @@ class mk:
merged_cls = merge_classes(f"mf-icon-{size}",
'icon-btn' if can_select else '',
'mmt-btn' if can_hover else '',
'flex items-center justify-center',
cls,
kwargs)