diff --git a/src/myfasthtml/assets/myfasthtml.js b/src/myfasthtml/assets/myfasthtml.js
index 75cea03..33b88ef 100644
--- a/src/myfasthtml/assets/myfasthtml.js
+++ b/src/myfasthtml/assets/myfasthtml.js
@@ -636,7 +636,7 @@ function updateTabs(controllerId) {
// Add key to current pressed keys
KeyboardRegistry.currentKeys.add(key);
- console.debug("Received key", key);
+ // console.debug("Received key", key);
// Create a snapshot of current keyboard state
const snapshot = new Set(KeyboardRegistry.currentKeys);
@@ -671,7 +671,7 @@ function updateTabs(controllerId) {
if (!currentNode) {
// No match in this tree, continue to next element
- console.debug("No match in tree for event", key);
+ // console.debug("No match in tree for event", key);
continue;
}
@@ -1289,7 +1289,7 @@ function updateTabs(controllerId) {
return;
}
- console.debug("Right-click on registered element", elementId);
+ //console.debug("Right-click on registered element", elementId);
// For right-click, clicked_inside is always true (we only trigger if clicked on element)
const clickedInside = true;
@@ -1322,7 +1322,7 @@ function updateTabs(controllerId) {
if (!currentNode) {
// No match in this tree
- console.debug("No match in tree for right-click");
+ //console.debug("No match in tree for right-click");
// Clear history for invalid sequences
MouseRegistry.snapshotHistory = [];
return;
@@ -1518,6 +1518,14 @@ function initDataGridScrollbars(gridId) {
return;
}
+ // Cleanup previous listeners if any
+ if (wrapper._scrollbarAbortController) {
+ wrapper._scrollbarAbortController.abort();
+ }
+ wrapper._scrollbarAbortController = new AbortController();
+ const signal = wrapper._scrollbarAbortController.signal;
+
+
const verticalScrollbar = wrapper.querySelector(".dt2-scrollbars-vertical");
const verticalWrapper = wrapper.querySelector(".dt2-scrollbars-vertical-wrapper");
const horizontalScrollbar = wrapper.querySelector(".dt2-scrollbars-horizontal");
@@ -1577,7 +1585,6 @@ function initDataGridScrollbars(gridId) {
};
// PHASE 2: Calculate all values
-
const contentWidth = Math.max(metrics.headerScrollWidth, metrics.bodyScrollWidth);
// Visibility
@@ -1649,7 +1656,7 @@ function initDataGridScrollbars(gridId) {
dragStartY = e.clientY;
dragStartScrollTop = cachedBodyScrollTop;
wrapper.setAttribute("mf-no-tooltip", "");
- });
+ }, { signal });
// Horizontal scrollbar mousedown
horizontalScrollbar.addEventListener("mousedown", (e) => {
@@ -1657,7 +1664,7 @@ function initDataGridScrollbars(gridId) {
dragStartX = e.clientX;
dragStartScrollLeft = cachedTableScrollLeft;
wrapper.setAttribute("mf-no-tooltip", "");
- });
+ }, { signal });
// Consolidated mousemove listener
document.addEventListener("mousemove", (e) => {
@@ -1688,7 +1695,7 @@ function initDataGridScrollbars(gridId) {
});
}
}
- });
+ }, { signal });
// Consolidated mouseup listener
document.addEventListener("mouseup", () => {
@@ -1699,7 +1706,7 @@ function initDataGridScrollbars(gridId) {
isDraggingHorizontal = false;
wrapper.removeAttribute("mf-no-tooltip");
}
- });
+ }, { signal });
// Wheel scrolling - OPTIMIZED with RAF throttling
let rafScheduledWheel = false;
@@ -1737,7 +1744,7 @@ function initDataGridScrollbars(gridId) {
event.preventDefault();
};
- wrapper.addEventListener("wheel", handleWheelScrolling, {passive: false});
+ wrapper.addEventListener("wheel", handleWheelScrolling, {passive: false, signal});
// Initialize scrollbars with single batched update
updateScrollbars();
@@ -1752,109 +1759,109 @@ function initDataGridScrollbars(gridId) {
updateScrollbars();
});
}
- });
+ }, { signal });
}
function makeDatagridColumnsResizable(datagridId) {
- console.debug("makeResizable on element " + datagridId);
+ //console.debug("makeResizable on element " + datagridId);
- const tableId = 't_' + datagridId;
- const table = document.getElementById(tableId);
- const resizeHandles = table.querySelectorAll('.dt2-resize-handle');
- const MIN_WIDTH = 30; // Prevent columns from becoming too narrow
+ const tableId = 't_' + datagridId;
+ const table = document.getElementById(tableId);
+ const resizeHandles = table.querySelectorAll('.dt2-resize-handle');
+ const MIN_WIDTH = 30; // Prevent columns from becoming too narrow
- // Attach event listeners using delegation
- resizeHandles.forEach(handle => {
- handle.addEventListener('mousedown', onStartResize);
- handle.addEventListener('touchstart', onStartResize, {passive: false});
- handle.addEventListener('dblclick', onDoubleClick); // Reset column width
+ // Attach event listeners using delegation
+ resizeHandles.forEach(handle => {
+ handle.addEventListener('mousedown', onStartResize);
+ handle.addEventListener('touchstart', onStartResize, {passive: false});
+ handle.addEventListener('dblclick', onDoubleClick); // Reset column width
+ });
+
+ let resizingState = null; // Maintain resizing state information
+
+ function onStartResize(event) {
+ event.preventDefault(); // Prevent unintended selections
+
+ const isTouch = event.type === 'touchstart';
+ const startX = isTouch ? event.touches[0].pageX : event.pageX;
+ const handle = event.target;
+ const cell = handle.parentElement;
+ const colIndex = cell.getAttribute('data-col');
+ const commandId = handle.dataset.commandId;
+ const cells = table.querySelectorAll(`.dt2-cell[data-col="${colIndex}"]`);
+
+ // Store initial state
+ const startWidth = cell.offsetWidth + 8;
+ resizingState = {startX, startWidth, colIndex, commandId, cells};
+
+ // Attach event listeners for resizing
+ document.addEventListener(isTouch ? 'touchmove' : 'mousemove', onResize);
+ document.addEventListener(isTouch ? 'touchend' : 'mouseup', onStopResize);
+ }
+
+ function onResize(event) {
+ if (!resizingState) {
+ return;
+ }
+
+ const isTouch = event.type === 'touchmove';
+ const currentX = isTouch ? event.touches[0].pageX : event.pageX;
+ const {startX, startWidth, cells} = resizingState;
+
+ // Calculate new width and apply constraints
+ const newWidth = Math.max(MIN_WIDTH, startWidth + (currentX - startX));
+ cells.forEach(cell => {
+ cell.style.width = `${newWidth}px`;
+ });
+ }
+
+ function onStopResize(event) {
+ if (!resizingState) {
+ return;
+ }
+
+ const {colIndex, commandId, cells} = resizingState;
+
+ const finalWidth = cells[0].offsetWidth;
+
+ // Send width update to server via HTMX
+ if (commandId) {
+ htmx.ajax('POST', '/myfasthtml/commands', {
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded"
+ },
+ swap: 'none',
+ values: {
+ c_id: commandId,
+ col_id: colIndex,
+ width: finalWidth
+ }
+ });
+ }
+
+ // Clean up
+ resizingState = null;
+ document.removeEventListener('mousemove', onResize);
+ document.removeEventListener('mouseup', onStopResize);
+ document.removeEventListener('touchmove', onResize);
+ document.removeEventListener('touchend', onStopResize);
+ }
+
+ function onDoubleClick(event) {
+ const handle = event.target;
+ const cell = handle.parentElement;
+ const colIndex = cell.getAttribute('data-col');
+ const cells = table.querySelectorAll(`.dt2-cell[data-col="${colIndex}"]`);
+
+ // Reset column width
+ cells.forEach(cell => {
+ cell.style.width = ''; // Use CSS default width
});
- let resizingState = null; // Maintain resizing state information
-
- function onStartResize(event) {
- event.preventDefault(); // Prevent unintended selections
-
- const isTouch = event.type === 'touchstart';
- const startX = isTouch ? event.touches[0].pageX : event.pageX;
- const handle = event.target;
- const cell = handle.parentElement;
- const colIndex = cell.getAttribute('data-col');
- const commandId = handle.dataset.commandId;
- const cells = table.querySelectorAll(`.dt2-cell[data-col="${colIndex}"]`);
-
- // Store initial state
- const startWidth = cell.offsetWidth + 8;
- resizingState = {startX, startWidth, colIndex, commandId, cells};
-
- // Attach event listeners for resizing
- document.addEventListener(isTouch ? 'touchmove' : 'mousemove', onResize);
- document.addEventListener(isTouch ? 'touchend' : 'mouseup', onStopResize);
- }
-
- function onResize(event) {
- if (!resizingState) {
- return;
- }
-
- const isTouch = event.type === 'touchmove';
- const currentX = isTouch ? event.touches[0].pageX : event.pageX;
- const {startX, startWidth, cells} = resizingState;
-
- // Calculate new width and apply constraints
- const newWidth = Math.max(MIN_WIDTH, startWidth + (currentX - startX));
- cells.forEach(cell => {
- cell.style.width = `${newWidth}px`;
- });
- }
-
- function onStopResize(event) {
- if (!resizingState) {
- return;
- }
-
- const {colIndex, commandId, cells} = resizingState;
-
- const finalWidth = cells[0].offsetWidth;
-
- // Send width update to server via HTMX
- if (commandId) {
- htmx.ajax('POST', '/myfasthtml/commands', {
- headers: {
- "Content-Type": "application/x-www-form-urlencoded"
- },
- swap: 'none',
- values: {
- c_id: commandId,
- col_id: colIndex,
- width: finalWidth
- }
- });
- }
-
- // Clean up
- resizingState = null;
- document.removeEventListener('mousemove', onResize);
- document.removeEventListener('mouseup', onStopResize);
- document.removeEventListener('touchmove', onResize);
- document.removeEventListener('touchend', onStopResize);
- }
-
- function onDoubleClick(event) {
- const handle = event.target;
- const cell = handle.parentElement;
- const colIndex = cell.getAttribute('data-col');
- const cells = table.querySelectorAll(`.dt2-cell[data-col="${colIndex}"]`);
-
- // Reset column width
- cells.forEach(cell => {
- cell.style.width = ''; // Use CSS default width
- });
-
- // Emit reset event
- const resetEvent = new CustomEvent('columnReset', {detail: {colIndex}});
- table.dispatchEvent(resetEvent);
- }
+ // Emit reset event
+ const resetEvent = new CustomEvent('columnReset', {detail: {colIndex}});
+ table.dispatchEvent(resetEvent);
+ }
}
/**
@@ -1863,84 +1870,84 @@ function makeDatagridColumnsResizable(datagridId) {
* @param {string} gridId - The DataGrid instance ID
*/
function makeDatagridColumnsMovable(gridId) {
- const table = document.getElementById(`t_${gridId}`);
- const headerRow = document.getElementById(`th_${gridId}`);
+ const table = document.getElementById(`t_${gridId}`);
+ const headerRow = document.getElementById(`th_${gridId}`);
- if (!table || !headerRow) {
- console.error(`DataGrid elements not found for ${gridId}`);
- return;
+ if (!table || !headerRow) {
+ console.error(`DataGrid elements not found for ${gridId}`);
+ return;
+ }
+
+ const moveCommandId = headerRow.dataset.moveCommandId;
+ const headerCells = headerRow.querySelectorAll('.dt2-cell:not(.dt2-col-hidden)');
+
+ let sourceColumn = null; // Column being dragged (original position)
+ let lastMoveTarget = null; // Last column we moved to (for persistence)
+ let hoverColumn = null; // Current hover target (for delayed move check)
+
+ headerCells.forEach(cell => {
+ cell.setAttribute('draggable', 'true');
+
+ // Prevent drag when clicking resize handle
+ const resizeHandle = cell.querySelector('.dt2-resize-handle');
+ if (resizeHandle) {
+ resizeHandle.addEventListener('mousedown', () => cell.setAttribute('draggable', 'false'));
+ resizeHandle.addEventListener('mouseup', () => cell.setAttribute('draggable', 'true'));
}
- const moveCommandId = headerRow.dataset.moveCommandId;
- const headerCells = headerRow.querySelectorAll('.dt2-cell:not(.dt2-col-hidden)');
-
- let sourceColumn = null; // Column being dragged (original position)
- let lastMoveTarget = null; // Last column we moved to (for persistence)
- let hoverColumn = null; // Current hover target (for delayed move check)
-
- headerCells.forEach(cell => {
- cell.setAttribute('draggable', 'true');
-
- // Prevent drag when clicking resize handle
- const resizeHandle = cell.querySelector('.dt2-resize-handle');
- if (resizeHandle) {
- resizeHandle.addEventListener('mousedown', () => cell.setAttribute('draggable', 'false'));
- resizeHandle.addEventListener('mouseup', () => cell.setAttribute('draggable', 'true'));
- }
-
- cell.addEventListener('dragstart', (e) => {
- sourceColumn = cell.getAttribute('data-col');
- lastMoveTarget = null;
- hoverColumn = null;
- e.dataTransfer.effectAllowed = 'move';
- e.dataTransfer.setData('text/plain', sourceColumn);
- cell.classList.add('dt2-dragging');
- });
-
- cell.addEventListener('dragenter', (e) => {
- e.preventDefault();
- const targetColumn = cell.getAttribute('data-col');
- hoverColumn = targetColumn;
-
- if (sourceColumn && sourceColumn !== targetColumn) {
- // Delay to skip columns when dragging fast
- setTimeout(() => {
- if (hoverColumn === targetColumn) {
- moveColumn(table, sourceColumn, targetColumn);
- lastMoveTarget = targetColumn;
- }
- }, 50);
- }
- });
-
- cell.addEventListener('dragover', (e) => {
- e.preventDefault();
- e.dataTransfer.dropEffect = 'move';
- });
-
- cell.addEventListener('drop', (e) => {
- e.preventDefault();
- // Persist to server
- if (moveCommandId && sourceColumn && lastMoveTarget) {
- htmx.ajax('POST', '/myfasthtml/commands', {
- headers: {"Content-Type": "application/x-www-form-urlencoded"},
- swap: 'none',
- values: {
- c_id: moveCommandId,
- source_col_id: sourceColumn,
- target_col_id: lastMoveTarget
- }
- });
- }
- });
-
- cell.addEventListener('dragend', () => {
- headerCells.forEach(c => c.classList.remove('dt2-dragging'));
- sourceColumn = null;
- lastMoveTarget = null;
- hoverColumn = null;
- });
+ cell.addEventListener('dragstart', (e) => {
+ sourceColumn = cell.getAttribute('data-col');
+ lastMoveTarget = null;
+ hoverColumn = null;
+ e.dataTransfer.effectAllowed = 'move';
+ e.dataTransfer.setData('text/plain', sourceColumn);
+ cell.classList.add('dt2-dragging');
});
+
+ cell.addEventListener('dragenter', (e) => {
+ e.preventDefault();
+ const targetColumn = cell.getAttribute('data-col');
+ hoverColumn = targetColumn;
+
+ if (sourceColumn && sourceColumn !== targetColumn) {
+ // Delay to skip columns when dragging fast
+ setTimeout(() => {
+ if (hoverColumn === targetColumn) {
+ moveColumn(table, sourceColumn, targetColumn);
+ lastMoveTarget = targetColumn;
+ }
+ }, 50);
+ }
+ });
+
+ cell.addEventListener('dragover', (e) => {
+ e.preventDefault();
+ e.dataTransfer.dropEffect = 'move';
+ });
+
+ cell.addEventListener('drop', (e) => {
+ e.preventDefault();
+ // Persist to server
+ if (moveCommandId && sourceColumn && lastMoveTarget) {
+ htmx.ajax('POST', '/myfasthtml/commands', {
+ headers: {"Content-Type": "application/x-www-form-urlencoded"},
+ swap: 'none',
+ values: {
+ c_id: moveCommandId,
+ source_col_id: sourceColumn,
+ target_col_id: lastMoveTarget
+ }
+ });
+ }
+ });
+
+ cell.addEventListener('dragend', () => {
+ headerCells.forEach(c => c.classList.remove('dt2-dragging'));
+ sourceColumn = null;
+ lastMoveTarget = null;
+ hoverColumn = null;
+ });
+ });
}
/**
@@ -1951,67 +1958,67 @@ function makeDatagridColumnsMovable(gridId) {
* @param {string} targetColId - Column ID to move next to
*/
function moveColumn(table, sourceColId, targetColId) {
- const ANIMATION_DURATION = 300; // Must match CSS transition duration
+ const ANIMATION_DURATION = 300; // Must match CSS transition duration
- const sourceHeader = table.querySelector(`.dt2-cell[data-col="${sourceColId}"]`);
- const targetHeader = table.querySelector(`.dt2-cell[data-col="${targetColId}"]`);
+ const sourceHeader = table.querySelector(`.dt2-cell[data-col="${sourceColId}"]`);
+ const targetHeader = table.querySelector(`.dt2-cell[data-col="${targetColId}"]`);
- if (!sourceHeader || !targetHeader) return;
- if (sourceHeader.classList.contains('dt2-moving')) return; // Animation in progress
+ if (!sourceHeader || !targetHeader) return;
+ if (sourceHeader.classList.contains('dt2-moving')) return; // Animation in progress
- const headerCells = Array.from(sourceHeader.parentNode.children);
- const sourceIdx = headerCells.indexOf(sourceHeader);
- const targetIdx = headerCells.indexOf(targetHeader);
+ const headerCells = Array.from(sourceHeader.parentNode.children);
+ const sourceIdx = headerCells.indexOf(sourceHeader);
+ const targetIdx = headerCells.indexOf(targetHeader);
- if (sourceIdx === targetIdx) return;
+ if (sourceIdx === targetIdx) return;
- const movingRight = sourceIdx < targetIdx;
- const sourceCells = table.querySelectorAll(`.dt2-cell[data-col="${sourceColId}"]`);
+ const movingRight = sourceIdx < targetIdx;
+ const sourceCells = table.querySelectorAll(`.dt2-cell[data-col="${sourceColId}"]`);
- // Collect cells that need to shift (between source and target)
- const cellsToShift = [];
- let shiftWidth = 0;
- const [startIdx, endIdx] = movingRight
- ? [sourceIdx + 1, targetIdx]
- : [targetIdx, sourceIdx - 1];
+ // Collect cells that need to shift (between source and target)
+ const cellsToShift = [];
+ let shiftWidth = 0;
+ const [startIdx, endIdx] = movingRight
+ ? [sourceIdx + 1, targetIdx]
+ : [targetIdx, sourceIdx - 1];
- for (let i = startIdx; i <= endIdx; i++) {
- const colId = headerCells[i].getAttribute('data-col');
- cellsToShift.push(...table.querySelectorAll(`.dt2-cell[data-col="${colId}"]`));
- shiftWidth += headerCells[i].offsetWidth;
- }
+ for (let i = startIdx; i <= endIdx; i++) {
+ const colId = headerCells[i].getAttribute('data-col');
+ cellsToShift.push(...table.querySelectorAll(`.dt2-cell[data-col="${colId}"]`));
+ shiftWidth += headerCells[i].offsetWidth;
+ }
- // Calculate animation distances
- const sourceWidth = sourceHeader.offsetWidth;
- const sourceDistance = movingRight ? shiftWidth : -shiftWidth;
- const shiftDistance = movingRight ? -sourceWidth : sourceWidth;
+ // Calculate animation distances
+ const sourceWidth = sourceHeader.offsetWidth;
+ const sourceDistance = movingRight ? shiftWidth : -shiftWidth;
+ const shiftDistance = movingRight ? -sourceWidth : sourceWidth;
- // Animate source column
- sourceCells.forEach(cell => {
- cell.classList.add('dt2-moving');
- cell.style.transform = `translateX(${sourceDistance}px)`;
+ // Animate source column
+ sourceCells.forEach(cell => {
+ cell.classList.add('dt2-moving');
+ cell.style.transform = `translateX(${sourceDistance}px)`;
+ });
+
+ // Animate shifted columns
+ cellsToShift.forEach(cell => {
+ cell.classList.add('dt2-moving');
+ cell.style.transform = `translateX(${shiftDistance}px)`;
+ });
+
+ // After animation: reset transforms and update DOM
+ setTimeout(() => {
+ [...sourceCells, ...cellsToShift].forEach(cell => {
+ cell.classList.remove('dt2-moving');
+ cell.style.transform = '';
});
- // Animate shifted columns
- cellsToShift.forEach(cell => {
- cell.classList.add('dt2-moving');
- cell.style.transform = `translateX(${shiftDistance}px)`;
+ // Move source column in DOM
+ table.querySelectorAll('.dt2-row').forEach(row => {
+ const sourceCell = row.querySelector(`[data-col="${sourceColId}"]`);
+ const targetCell = row.querySelector(`[data-col="${targetColId}"]`);
+ if (sourceCell && targetCell) {
+ movingRight ? targetCell.after(sourceCell) : targetCell.before(sourceCell);
+ }
});
-
- // After animation: reset transforms and update DOM
- setTimeout(() => {
- [...sourceCells, ...cellsToShift].forEach(cell => {
- cell.classList.remove('dt2-moving');
- cell.style.transform = '';
- });
-
- // Move source column in DOM
- table.querySelectorAll('.dt2-row').forEach(row => {
- const sourceCell = row.querySelector(`[data-col="${sourceColId}"]`);
- const targetCell = row.querySelector(`[data-col="${targetColId}"]`);
- if (sourceCell && targetCell) {
- movingRight ? targetCell.after(sourceCell) : targetCell.before(sourceCell);
- }
- });
- }, ANIMATION_DURATION);
+ }, ANIMATION_DURATION);
}
diff --git a/src/myfasthtml/controls/DataGrid.py b/src/myfasthtml/controls/DataGrid.py
index 2458269..12dee37 100644
--- a/src/myfasthtml/controls/DataGrid.py
+++ b/src/myfasthtml/controls/DataGrid.py
@@ -7,8 +7,10 @@ from typing import Optional
import pandas as pd
from fasthtml.common import NotStr
from fasthtml.components import *
+from pandas import DataFrame
from myfasthtml.controls.BaseCommands import BaseCommands
+from myfasthtml.controls.DataGridQuery import DataGridQuery, DG_QUERY_FILTER
from myfasthtml.controls.datagrid_objects import DataGridColumnState, DataGridRowState, \
DatagridSelectionState, DataGridHeaderFooterConf, DatagridEditionState
from myfasthtml.controls.helpers import mk
@@ -94,13 +96,20 @@ class Commands(BaseCommands):
self._owner,
self._owner.set_column_width
).htmx(target=None)
-
+
def move_column(self):
return Command("MoveColumn",
"Move column to new position",
self._owner,
self._owner.move_column
).htmx(target=None)
+
+ def filter(self):
+ return Command("Filter",
+ "Filter Grid",
+ self._owner,
+ self._owner.filter
+ )
class DataGrid(MultipleInstance):
@@ -110,11 +119,56 @@ 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
+ self._datagrid_filter = DataGridQuery(self)
+ self._datagrid_filter.bind_command("QueryChanged", self.commands.filter())
@property
def _df(self):
return self._state.ne_df
+ def _apply_sort(self, df):
+ if df is None:
+ return None
+
+ sorted_columns = []
+ sorted_asc = []
+ for sort_def in self._state.sorted:
+ if sort_def.direction != 0:
+ sorted_columns.append(sort_def.column_id)
+ asc = sort_def.direction == 1
+ sorted_asc.append(asc)
+
+ if sorted_columns:
+ df = df.sort_values(by=sorted_columns, ascending=sorted_asc)
+
+ return df
+
+ def _apply_filter(self, df):
+ if df is None:
+ return None
+
+ for col_id, values in self._state.filtered.items():
+ if col_id == FILTER_INPUT_CID and values is not None:
+ if self._datagrid_filter.get_query_type() == DG_QUERY_FILTER:
+ visible_columns = [c.col_id for c in self._state.columns if c.visible and c.col_id in df.columns]
+ df = df[df[visible_columns].map(lambda x: values.lower() in str(x).lower()).any(axis=1)]
+ else:
+ pass # we return all the row (but we will keep the highlight)
+
+ else:
+ df = df[df[col_id].astype(str).isin(values)]
+ return df
+
+ def _get_filtered_df(self):
+ if self._df is None:
+ return DataFrame()
+
+ df = self._df.copy()
+ df = self._apply_sort(df) # need to keep the real type to sort
+ df = self._apply_filter(df)
+
+ return df
+
def init_from_dataframe(self, df, init_state=True):
def _get_column_type(dtype):
@@ -181,13 +235,13 @@ class DataGrid(MultipleInstance):
if col.col_id == col_id:
col.width = int(width)
break
-
+
self._state.save()
-
+
def move_column(self, source_col_id: str, target_col_id: str):
"""Move column to new position. Called via Command from JS."""
logger.debug(f"move_column: {source_col_id=} {target_col_id=}")
-
+
# Find indices
source_idx = None
target_idx = None
@@ -196,14 +250,14 @@ class DataGrid(MultipleInstance):
source_idx = i
if col.col_id == target_col_id:
target_idx = i
-
+
if source_idx is None or target_idx is None:
logger.warning(f"move_column: column not found {source_col_id=} {target_col_id=}")
return
-
+
if source_idx == target_idx:
return
-
+
# Remove source column and insert at target position
col = self._state.columns.pop(source_idx)
# Adjust target index if source was before target
@@ -211,19 +265,24 @@ class DataGrid(MultipleInstance):
self._state.columns.insert(target_idx, col)
else:
self._state.columns.insert(target_idx, col)
-
+
self._state.save()
-
+
+ def filter(self):
+ logger.debug("filter")
+ self._state.filtered[FILTER_INPUT_CID] = self._datagrid_filter.get_query()
+ return self.mk_body_container(redraw_scrollbars=True)
+
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"),
cls="flex truncate cursor-default",
)
-
+
def _mk_header(col_def: DataGridColumnState):
return Div(
_mk_header_name(col_def),
@@ -233,7 +292,7 @@ class DataGrid(MultipleInstance):
data_tooltip=col_def.title,
cls="dt2-cell dt2-resizable flex",
)
-
+
header_class = "dt2-row dt2-header" + "" if self._settings.header_visible else " hidden"
return Div(
*[_mk_header(col_def) for col_def in self._state.columns],
@@ -268,9 +327,10 @@ class DataGrid(MultipleInstance):
res = []
if index > 0:
res.append(Span(value_str[:index], cls=f"{css_class}"))
- res.append(Span(value_str[index:index + len_keyword], cls=f"{css_class} dt2-highlight-1"))
+ res.append(Span(value_str[index:index + len_keyword], cls=f"dt2-highlight-1"))
if index + len_keyword < len(value_str):
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]
column_type = col_def.type
@@ -322,7 +382,7 @@ class DataGrid(MultipleInstance):
OPTIMIZED: Extract filter keyword once instead of 10,000 times.
OPTIMIZED: Uses OptimizedDiv for rows instead of Div for faster rendering.
"""
- df = self._df # self._get_filtered_df()
+ df = self._get_filtered_df()
start = page_index * DATAGRID_PAGE_SIZE
end = start + DATAGRID_PAGE_SIZE
if self._state.ns_total_rows > end:
@@ -344,6 +404,14 @@ class DataGrid(MultipleInstance):
return rows
+ def mk_body_container(self, redraw_scrollbars=False):
+ return Div(
+ self.mk_body(),
+ Script(f"initDataGridScrollbars('{self._id}');") if redraw_scrollbars else None,
+ cls="dt2-body-container",
+ id=f"tb_{self._id}"
+ )
+
def mk_body(self):
return Div(
*self.mk_body_content_page(0),
@@ -372,12 +440,9 @@ class DataGrid(MultipleInstance):
self.mk_headers(),
cls="dt2-header-container"
),
- # Body container - scroll via JS, scrollbars hidden
- Div(
- self.mk_body(),
- cls="dt2-body-container",
- id=f"tb_{self._id}"
- ),
+
+ self.mk_body_container(), # Body container - scroll via JS, scrollbars hidden
+
# Footer container - no scroll
Div(
self.mk_footers(),
@@ -470,13 +535,13 @@ class DataGrid(MultipleInstance):
if self._state.ne_df is None:
return Div("No data to display !")
- from myfasthtml.controls.DataGridFilter import DataGridFilter
return Div(
- Div(DataGridFilter(self), cls="mb-2"),
+ Div(self._datagrid_filter, cls="mb-2"),
self.mk_table(),
Script(f"initDataGrid('{self._id}');"),
id=self._id,
- style="height: 100%;"
+ cls="grid",
+ style="height: 100%; grid-template-rows: auto 1fr;"
)
def __ft__(self):
diff --git a/src/myfasthtml/controls/DataGridFilter.py b/src/myfasthtml/controls/DataGridFilter.py
deleted file mode 100644
index 85312bb..0000000
--- a/src/myfasthtml/controls/DataGridFilter.py
+++ /dev/null
@@ -1,76 +0,0 @@
-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()
diff --git a/src/myfasthtml/controls/DataGridQuery.py b/src/myfasthtml/controls/DataGridQuery.py
new file mode 100644
index 0000000..d9362a4
--- /dev/null
+++ b/src/myfasthtml/controls/DataGridQuery.py
@@ -0,0 +1,97 @@
+import logging
+from typing import Optional
+
+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")
+
+DG_QUERY_FILTER = "filter"
+DG_QUERY_SEARCH = "search"
+DG_QUERY_AI = "ai"
+
+query_type = {
+ DG_QUERY_FILTER: filter20_regular,
+ DG_QUERY_SEARCH: search20_regular,
+ DG_QUERY_AI: brain_circuit20_regular
+}
+
+
+class DataGridFilterState(DbObject):
+ def __init__(self, owner):
+ with self.initializing():
+ super().__init__(owner)
+ self.filter_type: str = "filter"
+ self.query: Optional[str] = None
+
+
+class Commands(BaseCommands):
+ def change_filter_type(self):
+ return Command("ChangeFilterType",
+ "Change filter type",
+ self._owner,
+ self._owner.change_query_type).htmx(target=f"#{self._id}")
+
+ def on_filter_changed(self):
+ return Command("QueryChanged",
+ "Query changed",
+ self._owner,
+ self._owner.query_changed).htmx(target=None)
+
+ def on_cancel_query(self):
+ return Command("CancelQuery",
+ "Cancel query",
+ self._owner,
+ self._owner.query_changed,
+ kwargs={"query": ""}
+ ).htmx(target=f"#{self._id}")
+
+
+class DataGridQuery(MultipleInstance):
+ def __init__(self, parent, _id=None):
+ super().__init__(parent, _id=_id or "-query")
+ self.commands = Commands(self)
+ self._state = DataGridFilterState(self)
+
+ def get_query(self):
+ return self._state.query
+
+ def get_query_type(self):
+ return self._state.filter_type
+
+ def change_query_type(self):
+ keys = list(query_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 query_changed(self, query):
+ logger.debug(f"query_changed {query=}")
+ self._state.query = query
+ return self
+
+ def render(self):
+ return Div(
+ mk.label(
+ Input(name="query",
+ value=self._state.query if self._state.query is not None else "",
+ placeholder="Search...",
+ **self.commands.on_filter_changed().get_htmx_params(values_encode="json")),
+ icon=mk.icon(query_type[self._state.filter_type], command=self.commands.change_filter_type()),
+ cls="input input-xs flex gap-3"
+ ),
+ mk.icon(dismiss_circle20_regular, size=24, command=self.commands.on_cancel_query()),
+ cls="flex",
+ id=self._id
+ )
+
+ def __ft__(self):
+ return self.render()
diff --git a/src/myfasthtml/core/commands.py b/src/myfasthtml/core/commands.py
index be0daef..236871d 100644
--- a/src/myfasthtml/core/commands.py
+++ b/src/myfasthtml/core/commands.py
@@ -14,6 +14,7 @@ logger = logging.getLogger("Commands")
AUTO_SWAP_OOB = "__auto_swap_oob__"
+
class Command:
"""
Represents the base command class for defining executable actions.
@@ -99,7 +100,7 @@ class Command:
def get_key(self):
return self._key
- def get_htmx_params(self, escaped=False):
+ def get_htmx_params(self, escaped=False, values_encode=None):
res = {
"hx-post": f"{ROUTE_ROOT}{Routes.Commands}",
"hx-swap": "outerHTML",
@@ -120,6 +121,9 @@ class Command:
if escaped:
res["hx-vals"] = html.escape(json.dumps(res["hx-vals"]))
+ if values_encode is "json":
+ res["hx-vals"] = json.dumps(res["hx-vals"])
+
return res
def execute(self, client_response: dict = None):
diff --git a/src/myfasthtml/core/optimized_ft.py b/src/myfasthtml/core/optimized_ft.py
index 58142ef..0bd6abe 100644
--- a/src/myfasthtml/core/optimized_ft.py
+++ b/src/myfasthtml/core/optimized_ft.py
@@ -7,7 +7,9 @@ by generating HTML strings directly instead of creating full FastHTML objects.
from functools import lru_cache
+from fastcore.xml import FT
from fasthtml.common import NotStr
+from fasthtml.components import Span
from myfasthtml.core.constants import NO_DEFAULT_VALUE
@@ -46,6 +48,8 @@ class OptimizedFt:
return item.to_html()
elif isinstance(item, NotStr):
return str(item)
+ elif isinstance(item, FT):
+ return str(item)
else:
raise Exception(f"Unsupported type: {type(item)}, {item=}")