Optimized Datagrid scrolling
This commit is contained in:
@@ -185,58 +185,72 @@ 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.");
|
||||
return;
|
||||
}
|
||||
// OPTIMIZATION C: Move ALL DOM reads into RAF to avoid forced synchronous layouts
|
||||
tooltipRafScheduled = true;
|
||||
requestAnimationFrame(() => {
|
||||
tooltipRafScheduled = false;
|
||||
|
||||
const content = cell.querySelector(".truncate") || cell;
|
||||
const isOverflowing = content.scrollWidth > content.clientWidth;
|
||||
const forceShow = cell.classList.contains("mf-tooltip");
|
||||
// Check again in case tooltip was disabled during RAF delay
|
||||
if (element.hasAttribute("mf-no-tooltip")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isOverflowing || forceShow) {
|
||||
const tooltipText = cell.getAttribute("data-tooltip");
|
||||
if (tooltipText) {
|
||||
const rect = cell.getBoundingClientRect();
|
||||
const tooltipRect = tooltipContainer.getBoundingClientRect();
|
||||
// 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");
|
||||
|
||||
let top = rect.top - 30; // Above the cell
|
||||
let left = rect.left;
|
||||
if (isOverflowing || forceShow) {
|
||||
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
|
||||
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
|
||||
}
|
||||
let top = rect.top - 30; // Above the cell
|
||||
let left = rect.left;
|
||||
|
||||
// Apply styles for tooltip positioning
|
||||
requestAnimationFrame(() => {
|
||||
// Adjust tooltip position to prevent it from going off-screen
|
||||
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.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(() => {
|
||||
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";
|
||||
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`;
|
||||
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`);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user