179 lines
4.5 KiB
JavaScript
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;
|