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, )