2 Commits

Author SHA1 Message Date
872d110f07 Working on DatagridFilter 2026-01-21 22:33:17 +01:00
ca40333742 Fixed toolip 2026-01-21 21:07:11 +01:00
4 changed files with 110 additions and 26 deletions

View File

@@ -838,14 +838,13 @@
.dt2-footer { .dt2-footer {
background-color: var(--color-base-200); background-color: var(--color-base-200);
border-radius: 10px 10px 0 0; border-radius: 10px 10px 0 0;
min-width: max-content; /* Content width propagates to scrollable parent */ min-width: max-content; /* Content width propagates to scrollable parent */
} }
/* Body */ /* Body */
.dt2-body { .dt2-body {
overflow: hidden; overflow: hidden;
font-size: 14px; min-width: max-content; /* Content width propagates to scrollable parent */
min-width: max-content; /* Content width propagates to scrollable parent */
} }
/* Row */ /* Row */
@@ -951,13 +950,17 @@
/* Table with Grid layout - horizontal scroll enabled, scrollbars hidden */ /* Table with Grid layout - horizontal scroll enabled, scrollbars hidden */
.dt2-table { .dt2-table {
--color-border: color-mix(in oklab, var(--color-base-content) 20%, #0000);
--color-resize: color-mix(in oklab, var(--color-base-content) 50%, #0000);
height: 100%; height: 100%;
display: grid; display: grid;
grid-template-rows: auto 1fr auto; /* header, body, footer */ grid-template-rows: auto 1fr auto; /* header, body, footer */
overflow-x: auto; /* Enable horizontal scroll */ overflow-x: auto; /* Enable horizontal scroll */
overflow-y: hidden; /* No vertical scroll on table */ overflow-y: hidden; /* No vertical scroll on table */
scrollbar-width: none; /* Firefox: hide scrollbar */ scrollbar-width: none; /* Firefox: hide scrollbar */
-ms-overflow-style: none; /* IE/Edge: hide scrollbar */ -ms-overflow-style: none; /* IE/Edge: hide scrollbar */
border: 1px solid var(--color-border);
border-radius: 10px;
} }
/* Chrome/Safari: hide scrollbar */ /* Chrome/Safari: hide scrollbar */
@@ -968,20 +971,20 @@
/* Header - no scroll, takes natural height */ /* Header - no scroll, takes natural height */
.dt2-header-container { .dt2-header-container {
overflow: hidden; overflow: hidden;
min-width: max-content; /* Force table to be as wide as content */ min-width: max-content; /* Force table to be as wide as content */
} }
/* Body - scrollable vertically via JS, scrollbars hidden */ /* Body - scrollable vertically via JS, scrollbars hidden */
.dt2-body-container { .dt2-body-container {
overflow: hidden; /* Scrollbars hidden, scroll via JS */ overflow: hidden; /* Scrollbars hidden, scroll via JS */
min-height: 0; /* Important for Grid to allow shrinking */ min-height: 0; /* Important for Grid to allow shrinking */
min-width: max-content; /* Force table to be as wide as content */ min-width: max-content; /* Force table to be as wide as content */
} }
/* Footer - no scroll, takes natural height */ /* Footer - no scroll, takes natural height */
.dt2-footer-container { .dt2-footer-container {
overflow: hidden; overflow: hidden;
min-width: max-content; /* Force table to be as wide as content */ min-width: max-content; /* Force table to be as wide as content */
} }
/* Custom scrollbars container - overlaid on table */ /* Custom scrollbars container - overlaid on table */
@@ -991,7 +994,7 @@
bottom: 0; bottom: 0;
left: 0; left: 0;
right: 0; right: 0;
pointer-events: none; /* Let clicks pass through */ pointer-events: none; /* Let clicks pass through */
z-index: 10; z-index: 10;
} }
@@ -1002,7 +1005,7 @@
background-color: var(--color-base-200); background-color: var(--color-base-200);
opacity: 1; opacity: 1;
transition: opacity 0.2s ease-in-out; transition: opacity 0.2s ease-in-out;
pointer-events: auto; /* Enable interaction */ pointer-events: auto; /* Enable interaction */
} }
/* Vertical scrollbar wrapper - right side, full table height */ /* Vertical scrollbar wrapper - right side, full table height */
@@ -1016,7 +1019,7 @@
/* Horizontal scrollbar wrapper - bottom, full width minus vertical scrollbar */ /* Horizontal scrollbar wrapper - bottom, full width minus vertical scrollbar */
.dt2-scrollbars-horizontal-wrapper { .dt2-scrollbars-horizontal-wrapper {
left: 0; left: 0;
right: 8px; /* Leave space for vertical scrollbar */ right: 8px; /* Leave space for vertical scrollbar */
bottom: 0; bottom: 0;
height: 8px; height: 8px;
} }

View File

@@ -190,7 +190,7 @@ function bindTooltipsWithDelegation(elementId) {
// Add a single mouseenter and mouseleave listener to the parent element // Add a single mouseenter and mouseleave listener to the parent element
element.addEventListener("mouseenter", (event) => { element.addEventListener("mouseenter", (event) => {
// OPTIMIZATION C: Early exit - check mf-no-tooltip FIRST (before any DOM work) // Early exit - check mf-no-tooltip FIRST (before any DOM work)
if (element.hasAttribute("mf-no-tooltip")) { if (element.hasAttribute("mf-no-tooltip")) {
return; return;
} }
@@ -243,14 +243,14 @@ function bindTooltipsWithDelegation(elementId) {
} }
} }
}); });
}); // OPTIMIZATION C: Removed capture phase (not needed) }, true); // Capture phase required: mouseenter doesn't bubble
element.addEventListener("mouseleave", (event) => { element.addEventListener("mouseleave", (event) => {
const cell = event.target.closest("[data-tooltip]"); const cell = event.target.closest("[data-tooltip]");
if (cell) { if (cell) {
tooltipContainer.setAttribute("data-visible", "false"); tooltipContainer.setAttribute("data-visible", "false");
} }
}); // OPTIMIZATION C: Removed capture phase (not needed) }, true); // Capture phase required: mouseleave doesn't bubble
} }
function initLayout(elementId) { function initLayout(elementId) {

View File

@@ -72,6 +72,7 @@ class DatagridSettings(DbObject):
self.views_visible: bool = True self.views_visible: bool = True
self.open_file_visible: bool = True self.open_file_visible: bool = True
self.open_settings_visible: bool = True self.open_settings_visible: bool = True
self.text_size: str = "sm"
class Commands(BaseCommands): class Commands(BaseCommands):
@@ -255,21 +256,21 @@ class DataGrid(MultipleInstance):
if not filter_keyword_lower: if not filter_keyword_lower:
# OPTIMIZATION: Return plain HTML string instead of Label object # OPTIMIZATION: Return plain HTML string instead of Label object
# Include "truncate text-sm" to match mk.label() behavior (ellipsis + font size) # Include "truncate text-sm" to match mk.label() behavior (ellipsis + font size)
return NotStr(f'<span class="{css_class} truncate text-sm">{value_str}</span>') return NotStr(f'<span class="{css_class} truncate">{value_str}</span>')
index = value_str.lower().find(filter_keyword_lower) index = value_str.lower().find(filter_keyword_lower)
if index < 0: if index < 0:
return NotStr(f'<span class="{css_class} truncate text-sm">{value_str}</span>') return NotStr(f'<span class="{css_class} truncate">{value_str}</span>')
# Has highlighting - need to use Span objects # Has highlighting - need to use Span objects
# Add "truncate text-sm" to match mk.label() behavior # Add "truncate text-sm" to match mk.label() behavior
len_keyword = len(filter_keyword_lower) len_keyword = len(filter_keyword_lower)
res = [] res = []
if index > 0: if index > 0:
res.append(Span(value_str[:index], cls=f"{css_class} text-sm")) res.append(Span(value_str[:index], cls=f"{css_class}"))
res.append(Span(value_str[index:index + len_keyword], cls=f"{css_class} text-sm dt2-highlight-1")) res.append(Span(value_str[index:index + len_keyword], cls=f"{css_class} dt2-highlight-1"))
if index + len_keyword < len(value_str): if index + len_keyword < len(value_str):
res.append(Span(value_str[index + len_keyword:], cls=f"{css_class} text-sm")) res.append(Span(value_str[index + len_keyword:], cls=f"{css_class}"))
return Span(*res, cls=f"{css_class} truncate") if len(res) > 1 else res[0] return Span(*res, cls=f"{css_class} truncate") if len(res) > 1 else res[0]
column_type = col_def.type column_type = col_def.type
@@ -281,7 +282,7 @@ class DataGrid(MultipleInstance):
# RowIndex - simplest case, just return the number as plain HTML # RowIndex - simplest case, just return the number as plain HTML
if column_type == ColumnType.RowIndex: if column_type == ColumnType.RowIndex:
return NotStr(f'<span class="dt2-cell-content-number truncate text-sm">{row_index}</span>') return NotStr(f'<span class="dt2-cell-content-number truncate">{row_index}</span>')
# Convert value to string # Convert value to string
value_str = str(value) value_str = str(value)
@@ -307,10 +308,12 @@ class DataGrid(MultipleInstance):
if not col_def.visible: if not col_def.visible:
return OptimizedDiv(cls="dt2-col-hidden") return OptimizedDiv(cls="dt2-col-hidden")
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)
return OptimizedDiv(content, return OptimizedDiv(content,
data_col=col_def.col_id, data_col=col_def.col_id,
data_tooltip=str(value),
style=f"width:{col_def.width}px;", style=f"width:{col_def.width}px;",
cls="dt2-cell") cls="dt2-cell")
@@ -344,7 +347,7 @@ class DataGrid(MultipleInstance):
def mk_body(self): def mk_body(self):
return Div( return Div(
*self.mk_body_content_page(0), *self.mk_body_content_page(0),
cls="dt2-body" cls=f"dt2-body text-{self._settings.text_size}",
) )
def mk_footers(self): def mk_footers(self):
@@ -467,7 +470,9 @@ class DataGrid(MultipleInstance):
if self._state.ne_df is None: if self._state.ne_df is None:
return Div("No data to display !") return Div("No data to display !")
from myfasthtml.controls.DataGridFilter import DataGridFilter
return Div( return Div(
Div(DataGridFilter(self), cls="mb-2"),
self.mk_table(), self.mk_table(),
Script(f"initDataGrid('{self._id}');"), Script(f"initDataGrid('{self._id}');"),
id=self._id, id=self._id,

View File

@@ -0,0 +1,76 @@
import logging
from fasthtml.components import *
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
from myfasthtml.icons.fluent import brain_circuit20_regular
from myfasthtml.icons.fluent_p1 import filter20_regular, search20_regular
from myfasthtml.icons.fluent_p2 import dismiss_circle20_regular
logger = logging.getLogger("DataGridFilter")
filter_type = {
"filter": filter20_regular,
"search": search20_regular,
"ai": brain_circuit20_regular
}
class DataGridFilterState(DbObject):
def __init__(self, owner):
with self.initializing():
super().__init__(owner)
self.filter_type: str = "filter"
class Commands(BaseCommands):
def change_filter_type(self):
return Command("ChangeFilterType",
"Change filter type",
self._owner,
self._owner.change_filter_type).htmx(target=f"#{self._id}")
def on_filter_changed(self):
return Command("FilterChanged",
"Filter changed",
self._owner,
self._owner.filter_changed).htmx(target=None)
class DataGridFilter(MultipleInstance):
def __init__(self, parent, _id=None):
super().__init__(parent, _id=_id or "-filter")
self.commands = Commands(self)
self._state = DataGridFilterState(self)
def change_filter_type(self):
keys = list(filter_type.keys()) # ["filter", "search", "ai"]
current_idx = keys.index(self._state.filter_type)
self._state.filter_type = keys[(current_idx + 1) % len(keys)]
return self
def filter_changed(self, f):
logger.debug(f"filter_changed {f=}")
return self
def render(self):
return Div(
mk.label(
Input(name="f",
placeholder="Search...",
**self.commands.on_filter_changed().get_htmx_params(escaped=True)),
icon=mk.icon(filter_type[self._state.filter_type], command=self.commands.change_filter_type()),
cls="input input-sm flex gap-2"
),
mk.icon(dismiss_circle20_regular, size=24),
# Keyboard(self, _id="-keyboard").add("enter", self.commands.on_filter_changed()),
cls="flex",
id=self._id
)
def __ft__(self):
return self.render()