Files
Rivertalk/assets/js/ui/core/scroll-manager.js
T
2026-02-12 17:10:03 -06:00

179 lines
4.5 KiB
JavaScript

// saladui/core/scroll-manager.js
/**
* ScrollManager utility for SaladUI components
* Manages scroll and resize event handlers with optimized performance
*/
class ScrollManager {
/**
* Create a scroll manager to handle scroll and resize events
*
* @param {Function} updateCallback - Function to call when scroll/resize events occur
* @param {Object} options - Additional options
*/
constructor(updateCallback, options = {}) {
this.updateCallback = updateCallback;
this.options = {
// Use requestAnimationFrame for throttling
useRAF: true,
...options,
};
this.scrollableParents = [];
this.active = false;
this.resizeObserver = null;
this.animationFrameId = null;
// Bind methods to maintain correct context
this.handleScroll = this.handleScroll.bind(this);
this.handleResize = this.handleResize.bind(this);
this.updatePosition = this.updatePosition.bind(this);
}
/**
* Start tracking scroll and resize events
*
* @param {HTMLElement} referenceElement - Element to track scrollable parents for
* @param {HTMLElement} targetElement - Optional element to observe with ResizeObserver
*/
start(referenceElement, targetElement = null) {
if (this.active) return;
// Find scrollable parent elements
if (referenceElement) {
this.scrollableParents = this.findScrollableParents(referenceElement);
// Add scroll listeners to all scrollable parents
this.scrollableParents.forEach((parent) => {
parent.addEventListener("scroll", this.handleScroll, { passive: true });
});
}
// Add resize listener
window.addEventListener("resize", this.handleResize, { passive: true });
// Set up ResizeObserver for element size changes if available
if (targetElement && typeof ResizeObserver !== "undefined") {
this.resizeObserver = new ResizeObserver(this.updatePosition);
this.resizeObserver.observe(targetElement);
if (referenceElement && referenceElement !== targetElement) {
this.resizeObserver.observe(referenceElement);
}
}
this.active = true;
}
/**
* Stop tracking scroll and resize events
*/
stop() {
if (!this.active) return;
// Remove scroll listeners
this.scrollableParents.forEach((parent) => {
parent.removeEventListener("scroll", this.handleScroll);
});
// Remove resize listener
window.removeEventListener("resize", this.handleResize);
// Disconnect ResizeObserver if present
if (this.resizeObserver) {
this.resizeObserver.disconnect();
this.resizeObserver = null;
}
// Cancel any pending animation frame
if (this.animationFrameId !== null) {
cancelAnimationFrame(this.animationFrameId);
this.animationFrameId = null;
}
this.active = false;
this.scrollableParents = [];
}
/**
* Handle scroll events with throttling
*/
handleScroll() {
if (this.options.useRAF) {
this.throttledUpdate();
} else {
this.updatePosition();
}
}
/**
* Handle resize events with throttling
*/
handleResize() {
if (this.options.useRAF) {
this.throttledUpdate();
} else {
this.updatePosition();
}
}
/**
* Throttle updates using requestAnimationFrame
*/
throttledUpdate() {
if (this.animationFrameId === null) {
this.animationFrameId = requestAnimationFrame(() => {
this.updatePosition();
this.animationFrameId = null;
});
}
}
/**
* Call the update callback
*/
updatePosition() {
if (this.updateCallback) {
this.updateCallback();
}
}
/**
* Find all scrollable parent elements
*/
findScrollableParents(element) {
const scrollableParents = [];
let currentElement = element;
while (currentElement && currentElement !== document.body) {
const style = window.getComputedStyle(currentElement);
if (
style.overflow === "auto" ||
style.overflow === "scroll" ||
style.overflowX === "auto" ||
style.overflowX === "scroll" ||
style.overflowY === "auto" ||
style.overflowY === "scroll"
) {
scrollableParents.push(currentElement);
}
currentElement = currentElement.parentElement;
}
// Always include window for global scrolling
scrollableParents.push(window);
return scrollableParents;
}
/**
* Clean up all references
*/
destroy() {
this.stop();
this.updateCallback = null;
this.options = null;
}
}
export default ScrollManager;