Working on DatagridFilter
This commit is contained in:
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -346,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):
|
||||||
@@ -469,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,
|
||||||
|
|||||||
76
src/myfasthtml/controls/DataGridFilter.py
Normal file
76
src/myfasthtml/controls/DataGridFilter.py
Normal 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()
|
||||||
Reference in New Issue
Block a user