// MgGridInfoPanel - Sticky scroll handling // Makes the InfoPanel sticky to viewport when scrolling window.MgGridInfoPanel = { observers: new Map(), // Initialize sticky behavior for an InfoPanel element initSticky: function (element, topOffset) { if (!element) return; const elementId = element.id || this.generateId(element); // Clean up existing observer if any this.disposeSticky(element); // Store the initial position of the element (relative to document) const rect = element.getBoundingClientRect(); const initialTop = rect.top + window.scrollY; // Calculate and set initial state this.updatePosition(element, initialTop); // Handler to update position on scroll and resize const updateHandler = () => { this.updatePosition(element, initialTop); }; // Add event listeners - use passive to not block scrolling window.addEventListener('resize', updateHandler, { passive: true }); window.addEventListener('scroll', updateHandler, { passive: true }); // Store cleanup info this.observers.set(elementId, { element: element, updateHandler: updateHandler, initialTop: initialTop }); return true; }, // Dispose sticky behavior disposeSticky: function (element) { if (!element) return; const elementId = element.id || this.findElementId(element); const observer = this.observers.get(elementId); if (observer) { window.removeEventListener('resize', observer.updateHandler); window.removeEventListener('scroll', observer.updateHandler); // Reset styles element.style.height = ''; element.style.maxHeight = ''; element.style.transform = ''; this.observers.delete(elementId); } }, // Update panel position and height based on scroll updatePosition: function (element, initialTop) { if (!element) return; const scrollY = window.scrollY; const viewportHeight = window.innerHeight; const bottomPadding = 30; // 30px from bottom // Calculate how much we've scrolled past the initial position const scrolledPast = Math.max(0, scrollY - initialTop); // Get the splitter pane to know our container limits const paneContainer = element.closest('.dxbl-splitter-pane'); let maxScrollOffset = Infinity; if (paneContainer) { // Don't scroll past the bottom of the pane const paneHeight = paneContainer.offsetHeight; const elementHeight = element.offsetHeight; maxScrollOffset = Math.max(0, paneHeight - elementHeight); } // Clamp the scroll offset const translateY = Math.min(scrolledPast, maxScrollOffset); // Apply transform to make it "sticky" element.style.transform = `translateY(${translateY}px)`; // Calculate height: from current visual position to viewport bottom const rect = element.getBoundingClientRect(); const visualTop = rect.top; // This already accounts for transform // Height from current visual top to viewport bottom minus padding const availableHeight = viewportHeight - visualTop - bottomPadding; // Clamp height const finalHeight = Math.max(200, Math.min(availableHeight, viewportHeight - bottomPadding)); element.style.height = finalHeight + 'px'; element.style.maxHeight = finalHeight + 'px'; }, // Generate a unique ID for the element generateId: function (element) { const id = 'mg-info-panel-' + Math.random().toString(36).substr(2, 9); element.id = id; return id; }, // Find element ID from stored observers findElementId: function (element) { for (const [id, observer] of this.observers.entries()) { if (observer.element === element) { return id; } } return null; } };