Optimized Datagrid scrolling

This commit is contained in:
2026-01-16 21:00:55 +01:00
parent d909f2125d
commit 500340fbd3

View File

@@ -185,58 +185,72 @@ function bindTooltipsWithDelegation(elementId) {
return; return;
} }
// OPTIMIZATION C: Throttling flag to limit mouseenter processing
let tooltipRafScheduled = false;
// 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) => {
//console.debug("Entering element", event.target) // OPTIMIZATION C: Early exit - check mf-no-tooltip FIRST (before any DOM work)
if (element.hasAttribute("mf-no-tooltip")) {
return;
}
// OPTIMIZATION C: Throttle mouseenter events (max 1 per frame)
if (tooltipRafScheduled) {
return;
}
const cell = event.target.closest("[data-tooltip]"); const cell = event.target.closest("[data-tooltip]");
if (!cell) { if (!cell) {
// console.debug(" No 'data-tooltip' attribute found. Stopping.");
return; return;
} }
const no_tooltip = element.hasAttribute("mf-no-tooltip"); // OPTIMIZATION C: Move ALL DOM reads into RAF to avoid forced synchronous layouts
if (no_tooltip) { tooltipRafScheduled = true;
// console.debug(" Attribute 'mmt-no-tooltip' found. Cancelling."); requestAnimationFrame(() => {
return; tooltipRafScheduled = false;
}
const content = cell.querySelector(".truncate") || cell; // Check again in case tooltip was disabled during RAF delay
const isOverflowing = content.scrollWidth > content.clientWidth; if (element.hasAttribute("mf-no-tooltip")) {
const forceShow = cell.classList.contains("mf-tooltip"); return;
}
if (isOverflowing || forceShow) { // All DOM reads happen here (batched in RAF)
const tooltipText = cell.getAttribute("data-tooltip"); const content = cell.querySelector(".truncate") || cell;
if (tooltipText) { const isOverflowing = content.scrollWidth > content.clientWidth;
const rect = cell.getBoundingClientRect(); const forceShow = cell.classList.contains("mf-tooltip");
const tooltipRect = tooltipContainer.getBoundingClientRect();
let top = rect.top - 30; // Above the cell if (isOverflowing || forceShow) {
let left = rect.left; const tooltipText = cell.getAttribute("data-tooltip");
if (tooltipText) {
const rect = cell.getBoundingClientRect();
const tooltipRect = tooltipContainer.getBoundingClientRect();
// Adjust tooltip position to prevent it from going off-screen let top = rect.top - 30; // Above the cell
if (top < 0) top = rect.bottom + 5; // Move below if no space above let left = rect.left;
if (left + tooltipRect.width > window.innerWidth) {
left = window.innerWidth - tooltipRect.width - 5; // Prevent overflow right
}
// Apply styles for tooltip positioning // Adjust tooltip position to prevent it from going off-screen
requestAnimationFrame(() => { if (top < 0) top = rect.bottom + 5; // Move below if no space above
if (left + tooltipRect.width > window.innerWidth) {
left = window.innerWidth - tooltipRect.width - 5; // Prevent overflow right
}
// Apply styles (already in RAF)
tooltipContainer.textContent = tooltipText; tooltipContainer.textContent = tooltipText;
tooltipContainer.setAttribute("data-visible", "true"); tooltipContainer.setAttribute("data-visible", "true");
tooltipContainer.style.top = `${top}px`; tooltipContainer.style.top = `${top}px`;
tooltipContainer.style.left = `${left}px`; tooltipContainer.style.left = `${left}px`;
}); }
} }
} });
}, true); // Use capture phase for better delegation if needed }); // OPTIMIZATION C: Removed capture phase (not needed)
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");
} }
}, true); // Use capture phase for better delegation if needed }); // OPTIMIZATION C: Removed capture phase (not needed)
} }
function initLayout(elementId) { function initLayout(elementId) {
@@ -1132,7 +1146,11 @@ function updateTabs(controllerId) {
* @param {MouseEvent} event - The mouse event * @param {MouseEvent} event - The mouse event
*/ */
function handleGlobalClick(event) { function handleGlobalClick(event) {
console.debug("Global click detected"); // DEBUG: Measure click handler performance
const clickStart = performance.now();
const elementCount = MouseRegistry.elements.size;
console.warn(`🖱️ Click handler START: processing ${elementCount} registered elements`);
// Create a snapshot of current mouse action with modifiers // Create a snapshot of current mouse action with modifiers
const snapshot = createSnapshot(event, 'click'); const snapshot = createSnapshot(event, 'click');
@@ -1151,8 +1169,10 @@ function updateTabs(controllerId) {
const currentMatches = []; const currentMatches = [];
let anyHasLongerSequence = false; let anyHasLongerSequence = false;
let foundAnyMatch = false; let foundAnyMatch = false;
let iterationCount = 0;
for (const [elementId, data] of MouseRegistry.elements) { for (const [elementId, data] of MouseRegistry.elements) {
iterationCount++;
const element = document.getElementById(elementId); const element = document.getElementById(elementId);
if (!element) continue; if (!element) continue;
@@ -1246,6 +1266,13 @@ function updateTabs(controllerId) {
if (MouseRegistry.snapshotHistory.length > 10) { if (MouseRegistry.snapshotHistory.length > 10) {
MouseRegistry.snapshotHistory = []; MouseRegistry.snapshotHistory = [];
} }
// DEBUG: Log click handler performance
const clickDuration = performance.now() - clickStart;
console.warn(`🖱️ Click handler DONE: ${clickDuration.toFixed(2)}ms (${iterationCount} iterations, ${currentMatches.length} matches)`);
if (clickDuration > 100) {
console.warn(`⚠️ SLOW CLICK HANDLER: ${clickDuration.toFixed(2)}ms for ${elementCount} elements`);
}
} }
/** /**
@@ -1478,8 +1505,6 @@ function updateTabs(controllerId) {
* @param {string} gridId - The ID of the DataGrid instance * @param {string} gridId - The ID of the DataGrid instance
*/ */
function initDataGridScrollbars(gridId) { function initDataGridScrollbars(gridId) {
console.debug("initDataGridScrollbars on element " + gridId);
const wrapper = document.getElementById(`tw_${gridId}`); const wrapper = document.getElementById(`tw_${gridId}`);
if (!wrapper) { if (!wrapper) {
@@ -1506,188 +1531,167 @@ function initDataGridScrollbars(gridId) {
// OPTIMIZATION: RequestAnimationFrame flags to throttle visual updates // OPTIMIZATION: RequestAnimationFrame flags to throttle visual updates
let rafScheduledVertical = false; let rafScheduledVertical = false;
let rafScheduledHorizontal = false; let rafScheduledHorizontal = false;
let rafScheduledUpdate = false;
const computeScrollbarVisibility = () => { // OPTIMIZATION: Pre-calculated scroll ratios (updated in updateScrollbars)
// Vertical: check if body content exceeds body container height // Allows instant mousedown with zero DOM reads
const isVerticalRequired = bodyContainer.scrollHeight > bodyContainer.clientHeight; let cachedVerticalScrollRatio = 0;
let cachedHorizontalScrollRatio = 0;
// Horizontal: check if content width exceeds table width (use cached references) // OPTIMIZATION: Cached scroll positions to avoid DOM reads in mousedown
const contentWidth = Math.max( // Initialized once at setup, updated in RAF handlers after each scroll change
header ? header.scrollWidth : 0, let cachedBodyScrollTop = bodyContainer.scrollTop;
body ? body.scrollWidth : 0 let cachedTableScrollLeft = table.scrollLeft;
);
const isHorizontalRequired = contentWidth > table.clientWidth; /**
* OPTIMIZED: Batched update function
* Phase 1: Read all DOM properties (no writes)
* Phase 2: Calculate all values
* Phase 3: Write all DOM properties in single RAF
*/
const updateScrollbars = () => {
if (rafScheduledUpdate) return;
rafScheduledUpdate = true;
requestAnimationFrame(() => { requestAnimationFrame(() => {
rafScheduledUpdate = false;
// PHASE 1: Read all DOM properties
const metrics = {
bodyScrollHeight: bodyContainer.scrollHeight,
bodyClientHeight: bodyContainer.clientHeight,
bodyScrollTop: bodyContainer.scrollTop,
tableClientWidth: table.clientWidth,
tableScrollLeft: table.scrollLeft,
verticalWrapperHeight: verticalWrapper.offsetHeight,
horizontalWrapperWidth: horizontalWrapper.offsetWidth,
headerScrollWidth: header ? header.scrollWidth : 0,
bodyScrollWidth: body ? body.scrollWidth : 0
};
// PHASE 2: Calculate all values
const contentWidth = Math.max(metrics.headerScrollWidth, metrics.bodyScrollWidth);
// Visibility
const isVerticalRequired = metrics.bodyScrollHeight > metrics.bodyClientHeight;
const isHorizontalRequired = contentWidth > metrics.tableClientWidth;
// Scrollbar sizes
let scrollbarHeight = 0;
if (metrics.bodyScrollHeight > 0) {
scrollbarHeight = (metrics.bodyClientHeight / metrics.bodyScrollHeight) * metrics.verticalWrapperHeight;
}
let scrollbarWidth = 0;
if (contentWidth > 0) {
scrollbarWidth = (metrics.tableClientWidth / contentWidth) * metrics.horizontalWrapperWidth;
}
// Scrollbar positions
const maxScrollTop = metrics.bodyScrollHeight - metrics.bodyClientHeight;
let verticalTop = 0;
if (maxScrollTop > 0) {
const scrollRatio = metrics.verticalWrapperHeight / metrics.bodyScrollHeight;
verticalTop = metrics.bodyScrollTop * scrollRatio;
}
const maxScrollLeft = contentWidth - metrics.tableClientWidth;
let horizontalLeft = 0;
if (maxScrollLeft > 0 && contentWidth > 0) {
const scrollRatio = metrics.horizontalWrapperWidth / contentWidth;
horizontalLeft = metrics.tableScrollLeft * scrollRatio;
}
// OPTIMIZATION: Pre-calculate and cache scroll ratios for instant mousedown
// Vertical scroll ratio
if (maxScrollTop > 0 && scrollbarHeight > 0) {
cachedVerticalScrollRatio = maxScrollTop / (metrics.verticalWrapperHeight - scrollbarHeight);
} else {
cachedVerticalScrollRatio = 0;
}
// Horizontal scroll ratio
if (maxScrollLeft > 0 && scrollbarWidth > 0) {
cachedHorizontalScrollRatio = maxScrollLeft / (metrics.horizontalWrapperWidth - scrollbarWidth);
} else {
cachedHorizontalScrollRatio = 0;
}
// PHASE 3: Write all DOM properties (already in RAF)
verticalWrapper.style.display = isVerticalRequired ? "block" : "none"; verticalWrapper.style.display = isVerticalRequired ? "block" : "none";
horizontalWrapper.style.display = isHorizontalRequired ? "block" : "none"; horizontalWrapper.style.display = isHorizontalRequired ? "block" : "none";
});
};
const computeScrollbarSize = () => {
// Vertical scrollbar height
const visibleHeight = bodyContainer.clientHeight;
const totalHeight = bodyContainer.scrollHeight;
const wrapperHeight = verticalWrapper.offsetHeight;
let scrollbarHeight = 0;
if (totalHeight > 0) {
scrollbarHeight = (visibleHeight / totalHeight) * wrapperHeight;
}
// Horizontal scrollbar width (use cached references)
const visibleWidth = table.clientWidth;
const totalWidth = Math.max(
header ? header.scrollWidth : 0,
body ? body.scrollWidth : 0
);
const wrapperWidth = horizontalWrapper.offsetWidth;
let scrollbarWidth = 0;
if (totalWidth > 0) {
scrollbarWidth = (visibleWidth / totalWidth) * wrapperWidth;
}
requestAnimationFrame(() => {
verticalScrollbar.style.height = `${scrollbarHeight}px`; verticalScrollbar.style.height = `${scrollbarHeight}px`;
horizontalScrollbar.style.width = `${scrollbarWidth}px`; horizontalScrollbar.style.width = `${scrollbarWidth}px`;
verticalScrollbar.style.top = `${verticalTop}px`;
horizontalScrollbar.style.left = `${horizontalLeft}px`;
}); });
}; };
const updateVerticalScrollbarPosition = () => { // Consolidated drag management
const maxScrollTop = bodyContainer.scrollHeight - bodyContainer.clientHeight;
const wrapperHeight = verticalWrapper.offsetHeight;
if (maxScrollTop > 0) {
const scrollRatio = wrapperHeight / bodyContainer.scrollHeight;
verticalScrollbar.style.top = `${bodyContainer.scrollTop * scrollRatio}px`;
}
};
const updateHorizontalScrollbarPosition = () => {
// Use cached references
const totalWidth = Math.max(
header ? header.scrollWidth : 0,
body ? body.scrollWidth : 0
);
const maxScrollLeft = totalWidth - table.clientWidth;
const wrapperWidth = horizontalWrapper.offsetWidth;
if (maxScrollLeft > 0 && totalWidth > 0) {
const scrollRatio = wrapperWidth / totalWidth;
horizontalScrollbar.style.left = `${table.scrollLeft * scrollRatio}px`;
}
};
// Drag management for vertical scrollbar
let isDraggingVertical = false; let isDraggingVertical = false;
let startYVertical = 0; let isDraggingHorizontal = false;
let pendingVerticalScroll = null; let dragStartY = 0;
let dragStartX = 0;
let dragStartScrollTop = 0;
let dragStartScrollLeft = 0;
// Vertical scrollbar mousedown
verticalScrollbar.addEventListener("mousedown", (e) => { verticalScrollbar.addEventListener("mousedown", (e) => {
isDraggingVertical = true; isDraggingVertical = true;
startYVertical = e.clientY; dragStartY = e.clientY;
document.body.style.userSelect = "none"; dragStartScrollTop = cachedBodyScrollTop;
verticalScrollbar.classList.add("dt2-dragging"); wrapper.setAttribute("mf-no-tooltip", "");
}); });
// Horizontal scrollbar mousedown
horizontalScrollbar.addEventListener("mousedown", (e) => {
isDraggingHorizontal = true;
dragStartX = e.clientX;
dragStartScrollLeft = cachedTableScrollLeft;
wrapper.setAttribute("mf-no-tooltip", "");
});
// Consolidated mousemove listener
document.addEventListener("mousemove", (e) => { document.addEventListener("mousemove", (e) => {
if (isDraggingVertical) { if (isDraggingVertical) {
const deltaY = e.clientY - startYVertical; const deltaY = e.clientY - dragStartY;
startYVertical = e.clientY;
// OPTIMIZATION: Store the scroll delta, update visual in RAF
if (pendingVerticalScroll === null) {
pendingVerticalScroll = 0;
}
pendingVerticalScroll += deltaY;
if (!rafScheduledVertical) { if (!rafScheduledVertical) {
rafScheduledVertical = true; rafScheduledVertical = true;
requestAnimationFrame(() => { requestAnimationFrame(() => {
rafScheduledVertical = false; rafScheduledVertical = false;
const scrollDelta = deltaY * cachedVerticalScrollRatio;
const wrapperHeight = verticalWrapper.offsetHeight; bodyContainer.scrollTop = dragStartScrollTop + scrollDelta;
const scrollbarHeight = verticalScrollbar.offsetHeight; cachedBodyScrollTop = bodyContainer.scrollTop;
const maxScrollTop = bodyContainer.scrollHeight - bodyContainer.clientHeight; updateScrollbars();
const scrollRatio = maxScrollTop / (wrapperHeight - scrollbarHeight);
let newTop = parseFloat(verticalScrollbar.style.top || "0") + pendingVerticalScroll;
newTop = Math.max(0, Math.min(newTop, wrapperHeight - scrollbarHeight));
verticalScrollbar.style.top = `${newTop}px`;
bodyContainer.scrollTop = newTop * scrollRatio;
pendingVerticalScroll = null;
}); });
} }
} } else if (isDraggingHorizontal) {
}); const deltaX = e.clientX - dragStartX;
document.addEventListener("mouseup", () => {
if (isDraggingVertical) {
isDraggingVertical = false;
document.body.style.userSelect = "";
verticalScrollbar.classList.remove("dt2-dragging");
}
});
// Drag management for horizontal scrollbar
let isDraggingHorizontal = false;
let startXHorizontal = 0;
let pendingHorizontalScroll = null;
horizontalScrollbar.addEventListener("mousedown", (e) => {
isDraggingHorizontal = true;
startXHorizontal = e.clientX;
document.body.style.userSelect = "none";
horizontalScrollbar.classList.add("dt2-dragging");
});
document.addEventListener("mousemove", (e) => {
if (isDraggingHorizontal) {
const deltaX = e.clientX - startXHorizontal;
startXHorizontal = e.clientX;
// OPTIMIZATION: Store the scroll delta, update visual in RAF
if (pendingHorizontalScroll === null) {
pendingHorizontalScroll = 0;
}
pendingHorizontalScroll += deltaX;
if (!rafScheduledHorizontal) { if (!rafScheduledHorizontal) {
rafScheduledHorizontal = true; rafScheduledHorizontal = true;
requestAnimationFrame(() => { requestAnimationFrame(() => {
rafScheduledHorizontal = false; rafScheduledHorizontal = false;
const scrollDelta = deltaX * cachedHorizontalScrollRatio;
const wrapperWidth = horizontalWrapper.offsetWidth; table.scrollLeft = dragStartScrollLeft + scrollDelta;
const scrollbarWidth = horizontalScrollbar.offsetWidth; cachedTableScrollLeft = table.scrollLeft;
updateScrollbars();
// Use cached references
const totalWidth = Math.max(
header ? header.scrollWidth : 0,
body ? body.scrollWidth : 0
);
const maxScrollLeft = totalWidth - table.clientWidth;
const scrollRatio = maxScrollLeft / (wrapperWidth - scrollbarWidth);
let newLeft = parseFloat(horizontalScrollbar.style.left || "0") + pendingHorizontalScroll;
newLeft = Math.max(0, Math.min(newLeft, wrapperWidth - scrollbarWidth));
horizontalScrollbar.style.left = `${newLeft}px`;
table.scrollLeft = newLeft * scrollRatio;
pendingHorizontalScroll = null;
}); });
} }
} }
}); });
// Consolidated mouseup listener
document.addEventListener("mouseup", () => { document.addEventListener("mouseup", () => {
if (isDraggingHorizontal) { if (isDraggingVertical) {
isDraggingVertical = false;
wrapper.removeAttribute("mf-no-tooltip");
} else if (isDraggingHorizontal) {
isDraggingHorizontal = false; isDraggingHorizontal = false;
document.body.style.userSelect = ""; wrapper.removeAttribute("mf-no-tooltip");
horizontalScrollbar.classList.remove("dt2-dragging");
} }
}); });
@@ -1711,13 +1715,16 @@ function initDataGridScrollbars(gridId) {
bodyContainer.scrollTop += pendingWheelDeltaY; bodyContainer.scrollTop += pendingWheelDeltaY;
table.scrollLeft += pendingWheelDeltaX; table.scrollLeft += pendingWheelDeltaX;
// Update scrollbar positions // Update caches with clamped values (read back from DOM in RAF - OK)
updateVerticalScrollbarPosition(); cachedBodyScrollTop = bodyContainer.scrollTop;
updateHorizontalScrollbarPosition(); cachedTableScrollLeft = table.scrollLeft;
// Reset pending deltas // Reset pending deltas
pendingWheelDeltaX = 0; pendingWheelDeltaX = 0;
pendingWheelDeltaY = 0; pendingWheelDeltaY = 0;
// Update all scrollbars in a single batched operation
updateScrollbars();
}); });
} }
@@ -1726,20 +1733,19 @@ function initDataGridScrollbars(gridId) {
wrapper.addEventListener("wheel", handleWheelScrolling, { passive: false }); wrapper.addEventListener("wheel", handleWheelScrolling, { passive: false });
// Initialize scrollbars // Initialize scrollbars with single batched update
computeScrollbarVisibility(); updateScrollbars();
computeScrollbarSize();
updateVerticalScrollbarPosition();
updateHorizontalScrollbarPosition();
// Recompute on window resize // Recompute on window resize with RAF throttling
let resizeScheduled = false;
window.addEventListener("resize", () => { window.addEventListener("resize", () => {
computeScrollbarVisibility(); if (!resizeScheduled) {
computeScrollbarSize(); resizeScheduled = true;
updateVerticalScrollbarPosition(); requestAnimationFrame(() => {
updateHorizontalScrollbarPosition(); resizeScheduled = false;
updateScrollbars();
});
}
}); });
console.info(`DataGrid "${gridId}" initialized with custom scrollbars`);
} }