Optimized Datagrid scrolling

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

View File

@@ -185,22 +185,37 @@ function bindTooltipsWithDelegation(elementId) {
return;
}
// OPTIMIZATION C: Throttling flag to limit mouseenter processing
let tooltipRafScheduled = false;
// Add a single mouseenter and mouseleave listener to the parent element
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]");
if (!cell) {
// console.debug(" No 'data-tooltip' attribute found. Stopping.");
return;
}
const no_tooltip = element.hasAttribute("mf-no-tooltip");
if (no_tooltip) {
// console.debug(" Attribute 'mmt-no-tooltip' found. Cancelling.");
// OPTIMIZATION C: Move ALL DOM reads into RAF to avoid forced synchronous layouts
tooltipRafScheduled = true;
requestAnimationFrame(() => {
tooltipRafScheduled = false;
// Check again in case tooltip was disabled during RAF delay
if (element.hasAttribute("mf-no-tooltip")) {
return;
}
// All DOM reads happen here (batched in RAF)
const content = cell.querySelector(".truncate") || cell;
const isOverflowing = content.scrollWidth > content.clientWidth;
const forceShow = cell.classList.contains("mf-tooltip");
@@ -220,23 +235,22 @@ function bindTooltipsWithDelegation(elementId) {
left = window.innerWidth - tooltipRect.width - 5; // Prevent overflow right
}
// Apply styles for tooltip positioning
requestAnimationFrame(() => {
// Apply styles (already in RAF)
tooltipContainer.textContent = tooltipText;
tooltipContainer.setAttribute("data-visible", "true");
tooltipContainer.style.top = `${top}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) => {
const cell = event.target.closest("[data-tooltip]");
if (cell) {
tooltipContainer.setAttribute("data-visible", "false");
}
}, true); // Use capture phase for better delegation if needed
}); // OPTIMIZATION C: Removed capture phase (not needed)
}
function initLayout(elementId) {
@@ -1132,7 +1146,11 @@ function updateTabs(controllerId) {
* @param {MouseEvent} event - The mouse 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
const snapshot = createSnapshot(event, 'click');
@@ -1151,8 +1169,10 @@ function updateTabs(controllerId) {
const currentMatches = [];
let anyHasLongerSequence = false;
let foundAnyMatch = false;
let iterationCount = 0;
for (const [elementId, data] of MouseRegistry.elements) {
iterationCount++;
const element = document.getElementById(elementId);
if (!element) continue;
@@ -1246,6 +1266,13 @@ function updateTabs(controllerId) {
if (MouseRegistry.snapshotHistory.length > 10) {
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
*/
function initDataGridScrollbars(gridId) {
console.debug("initDataGridScrollbars on element " + gridId);
const wrapper = document.getElementById(`tw_${gridId}`);
if (!wrapper) {
@@ -1506,188 +1531,167 @@ function initDataGridScrollbars(gridId) {
// OPTIMIZATION: RequestAnimationFrame flags to throttle visual updates
let rafScheduledVertical = false;
let rafScheduledHorizontal = false;
let rafScheduledUpdate = false;
const computeScrollbarVisibility = () => {
// Vertical: check if body content exceeds body container height
const isVerticalRequired = bodyContainer.scrollHeight > bodyContainer.clientHeight;
// OPTIMIZATION: Pre-calculated scroll ratios (updated in updateScrollbars)
// Allows instant mousedown with zero DOM reads
let cachedVerticalScrollRatio = 0;
let cachedHorizontalScrollRatio = 0;
// Horizontal: check if content width exceeds table width (use cached references)
const contentWidth = Math.max(
header ? header.scrollWidth : 0,
body ? body.scrollWidth : 0
);
const isHorizontalRequired = contentWidth > table.clientWidth;
// OPTIMIZATION: Cached scroll positions to avoid DOM reads in mousedown
// Initialized once at setup, updated in RAF handlers after each scroll change
let cachedBodyScrollTop = bodyContainer.scrollTop;
let cachedTableScrollLeft = table.scrollLeft;
/**
* 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(() => {
verticalWrapper.style.display = isVerticalRequired ? "block" : "none";
horizontalWrapper.style.display = isHorizontalRequired ? "block" : "none";
});
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
};
const computeScrollbarSize = () => {
// Vertical scrollbar height
const visibleHeight = bodyContainer.clientHeight;
const totalHeight = bodyContainer.scrollHeight;
const wrapperHeight = verticalWrapper.offsetHeight;
// 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 (totalHeight > 0) {
scrollbarHeight = (visibleHeight / totalHeight) * wrapperHeight;
if (metrics.bodyScrollHeight > 0) {
scrollbarHeight = (metrics.bodyClientHeight / metrics.bodyScrollHeight) * metrics.verticalWrapperHeight;
}
// 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;
if (contentWidth > 0) {
scrollbarWidth = (metrics.tableClientWidth / contentWidth) * metrics.horizontalWrapperWidth;
}
requestAnimationFrame(() => {
// 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";
horizontalWrapper.style.display = isHorizontalRequired ? "block" : "none";
verticalScrollbar.style.height = `${scrollbarHeight}px`;
horizontalScrollbar.style.width = `${scrollbarWidth}px`;
verticalScrollbar.style.top = `${verticalTop}px`;
horizontalScrollbar.style.left = `${horizontalLeft}px`;
});
};
const updateVerticalScrollbarPosition = () => {
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
// Consolidated drag management
let isDraggingVertical = false;
let startYVertical = 0;
let pendingVerticalScroll = null;
let isDraggingHorizontal = false;
let dragStartY = 0;
let dragStartX = 0;
let dragStartScrollTop = 0;
let dragStartScrollLeft = 0;
// Vertical scrollbar mousedown
verticalScrollbar.addEventListener("mousedown", (e) => {
isDraggingVertical = true;
startYVertical = e.clientY;
document.body.style.userSelect = "none";
verticalScrollbar.classList.add("dt2-dragging");
dragStartY = e.clientY;
dragStartScrollTop = cachedBodyScrollTop;
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) => {
if (isDraggingVertical) {
const deltaY = e.clientY - startYVertical;
startYVertical = e.clientY;
// OPTIMIZATION: Store the scroll delta, update visual in RAF
if (pendingVerticalScroll === null) {
pendingVerticalScroll = 0;
}
pendingVerticalScroll += deltaY;
const deltaY = e.clientY - dragStartY;
if (!rafScheduledVertical) {
rafScheduledVertical = true;
requestAnimationFrame(() => {
rafScheduledVertical = false;
const wrapperHeight = verticalWrapper.offsetHeight;
const scrollbarHeight = verticalScrollbar.offsetHeight;
const maxScrollTop = bodyContainer.scrollHeight - bodyContainer.clientHeight;
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;
const scrollDelta = deltaY * cachedVerticalScrollRatio;
bodyContainer.scrollTop = dragStartScrollTop + scrollDelta;
cachedBodyScrollTop = bodyContainer.scrollTop;
updateScrollbars();
});
}
}
});
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;
} else if (isDraggingHorizontal) {
const deltaX = e.clientX - dragStartX;
if (!rafScheduledHorizontal) {
rafScheduledHorizontal = true;
requestAnimationFrame(() => {
rafScheduledHorizontal = false;
const wrapperWidth = horizontalWrapper.offsetWidth;
const scrollbarWidth = horizontalScrollbar.offsetWidth;
// 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;
const scrollDelta = deltaX * cachedHorizontalScrollRatio;
table.scrollLeft = dragStartScrollLeft + scrollDelta;
cachedTableScrollLeft = table.scrollLeft;
updateScrollbars();
});
}
}
});
// Consolidated mouseup listener
document.addEventListener("mouseup", () => {
if (isDraggingHorizontal) {
if (isDraggingVertical) {
isDraggingVertical = false;
wrapper.removeAttribute("mf-no-tooltip");
} else if (isDraggingHorizontal) {
isDraggingHorizontal = false;
document.body.style.userSelect = "";
horizontalScrollbar.classList.remove("dt2-dragging");
wrapper.removeAttribute("mf-no-tooltip");
}
});
@@ -1711,13 +1715,16 @@ function initDataGridScrollbars(gridId) {
bodyContainer.scrollTop += pendingWheelDeltaY;
table.scrollLeft += pendingWheelDeltaX;
// Update scrollbar positions
updateVerticalScrollbarPosition();
updateHorizontalScrollbarPosition();
// Update caches with clamped values (read back from DOM in RAF - OK)
cachedBodyScrollTop = bodyContainer.scrollTop;
cachedTableScrollLeft = table.scrollLeft;
// Reset pending deltas
pendingWheelDeltaX = 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 });
// Initialize scrollbars
computeScrollbarVisibility();
computeScrollbarSize();
updateVerticalScrollbarPosition();
updateHorizontalScrollbarPosition();
// Initialize scrollbars with single batched update
updateScrollbars();
// Recompute on window resize
// Recompute on window resize with RAF throttling
let resizeScheduled = false;
window.addEventListener("resize", () => {
computeScrollbarVisibility();
computeScrollbarSize();
updateVerticalScrollbarPosition();
updateHorizontalScrollbarPosition();
if (!resizeScheduled) {
resizeScheduled = true;
requestAnimationFrame(() => {
resizeScheduled = false;
updateScrollbars();
});
}
});
console.info(`DataGrid "${gridId}" initialized with custom scrollbars`);
}