diff --git a/src/myfasthtml/assets/myfasthtml.js b/src/myfasthtml/assets/myfasthtml.js
index a431335..376544c 100644
--- a/src/myfasthtml/assets/myfasthtml.js
+++ b/src/myfasthtml/assets/myfasthtml.js
@@ -1477,8 +1477,87 @@ function initDataGrid(gridId) {
updateDatagridSelection(gridId)
}
-function initDataGridMouseOver(gridId) {
+/**
+ * Initialize DataGrid hover effects using event delegation.
+ *
+ * Optimizations:
+ * - Event delegation: 1 listener instead of NĂ—2 (where N = number of cells)
+ * - Row mode: O(1) via class toggle on parent row
+ * - Column mode: RAF batching + cached cells for efficient class removal
+ * - Works with HTMX swaps: listener on stable parent, querySelectorAll finds new cells
+ * - No mouseout: hover selection stays visible when leaving the table
+ *
+ * @param {string} gridId - The DataGrid instance ID
+ */
+function initDataGridMouseOver(gridId) {
+ const table = document.getElementById(`t_${gridId}`);
+ if (!table) {
+ console.error(`Table with id "t_${gridId}" not found.`);
+ return;
+ }
+
+ const wrapper = document.getElementById(`tw_${gridId}`);
+ const selectionModeDiv = document.getElementById(`tsm_${gridId}`);
+
+ // Track hover state
+ let currentHoverRow = null;
+ let currentHoverColId = null;
+ let currentHoverColCells = null;
+
+ table.addEventListener('mouseover', (e) => {
+ // Skip hover during scrolling
+ if (wrapper?.hasAttribute('mf-no-hover')) return;
+
+ const cell = e.target.closest('.dt2-cell');
+ if (!cell) return;
+
+ const selectionMode = selectionModeDiv?.getAttribute('selection-mode');
+
+ if (selectionMode === 'row') {
+ const rowElement = cell.parentElement;
+ if (rowElement !== currentHoverRow) {
+ if (currentHoverRow) {
+ currentHoverRow.classList.remove('dt2-hover-row');
+ }
+ rowElement.classList.add('dt2-hover-row');
+ currentHoverRow = rowElement;
+ }
+ } else if (selectionMode === 'column') {
+ const colId = cell.dataset.col;
+
+ // Skip if same column
+ if (colId === currentHoverColId) return;
+
+ requestAnimationFrame(() => {
+ // Remove old column highlight
+ if (currentHoverColCells) {
+ currentHoverColCells.forEach(c => c.classList.remove('dt2-hover-column'));
+ }
+
+ // Query and add new column highlight
+ currentHoverColCells = table.querySelectorAll(`.dt2-cell[data-col="${colId}"]`);
+ currentHoverColCells.forEach(c => c.classList.add('dt2-hover-column'));
+
+ currentHoverColId = colId;
+ });
+ }
+ });
+
+ // Clean up when leaving the table entirely
+ table.addEventListener('mouseout', (e) => {
+ if (!table.contains(e.relatedTarget)) {
+ if (currentHoverRow) {
+ currentHoverRow.classList.remove('dt2-hover-row');
+ currentHoverRow = null;
+ }
+ if (currentHoverColCells) {
+ currentHoverColCells.forEach(c => c.classList.remove('dt2-hover-column'));
+ currentHoverColCells = null;
+ currentHoverColId = null;
+ }
+ }
+ });
}
/**
@@ -1638,6 +1717,7 @@ function initDataGridScrollbars(gridId) {
dragStartY = e.clientY;
dragStartScrollTop = cachedBodyScrollTop;
wrapper.setAttribute("mf-no-tooltip", "");
+ wrapper.setAttribute("mf-no-hover", "");
}, {signal});
// Horizontal scrollbar mousedown
@@ -1646,6 +1726,7 @@ function initDataGridScrollbars(gridId) {
dragStartX = e.clientX;
dragStartScrollLeft = cachedTableScrollLeft;
wrapper.setAttribute("mf-no-tooltip", "");
+ wrapper.setAttribute("mf-no-hover", "");
}, {signal});
// Consolidated mousemove listener
@@ -1684,9 +1765,11 @@ function initDataGridScrollbars(gridId) {
if (isDraggingVertical) {
isDraggingVertical = false;
wrapper.removeAttribute("mf-no-tooltip");
+ wrapper.removeAttribute("mf-no-hover");
} else if (isDraggingHorizontal) {
isDraggingHorizontal = false;
wrapper.removeAttribute("mf-no-tooltip");
+ wrapper.removeAttribute("mf-no-hover");
}
}, {signal});
@@ -1694,8 +1777,20 @@ function initDataGridScrollbars(gridId) {
let rafScheduledWheel = false;
let pendingWheelDeltaX = 0;
let pendingWheelDeltaY = 0;
+ let wheelEndTimeout = null;
const handleWheelScrolling = (event) => {
+ // Disable hover and tooltip during wheel scroll
+ wrapper.setAttribute("mf-no-hover", "");
+ wrapper.setAttribute("mf-no-tooltip", "");
+
+ // Clear previous timeout and re-enable after 150ms of no wheel events
+ if (wheelEndTimeout) clearTimeout(wheelEndTimeout);
+ wheelEndTimeout = setTimeout(() => {
+ wrapper.removeAttribute("mf-no-hover");
+ wrapper.removeAttribute("mf-no-tooltip");
+ }, 150);
+
// Accumulate wheel deltas
pendingWheelDeltaX += event.deltaX;
pendingWheelDeltaY += event.deltaY;
diff --git a/src/myfasthtml/controls/DataGrid.py b/src/myfasthtml/controls/DataGrid.py
index 83acc0e..77ffbae 100644
--- a/src/myfasthtml/controls/DataGrid.py
+++ b/src/myfasthtml/controls/DataGrid.py
@@ -179,6 +179,7 @@ class DataGrid(MultipleInstance):
df = self._df.copy()
df = self._apply_sort(df) # need to keep the real type to sort
df = self._apply_filter(df)
+ self._state.ns_total_rows = len(df)
return df
@@ -533,7 +534,8 @@ 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"{self._state.selection.selection_mode}",
+ selection_mode=f"column",
**extra_attr,
)