diff --git a/src/myfasthtml/assets/myfasthtml.js b/src/myfasthtml/assets/myfasthtml.js index f0a8c0a..16e159b 100644 --- a/src/myfasthtml/assets/myfasthtml.js +++ b/src/myfasthtml/assets/myfasthtml.js @@ -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`); }