UNPKG

stylescape

Version:

Stylescape is a visual identity framework developed by Scape Agency.

189 lines 6.85 kB
export class ScrollSpyManager { constructor(options = {}) { this.sections = []; this.navLinks = []; this.ticking = false; this.currentActiveId = null; this.handleScroll = () => { if (!this.ticking) { window.requestAnimationFrame(() => { this.updateActiveLink(); this.ticking = false; }); this.ticking = true; } }; this.handleLinkClick = (event) => { const link = event.currentTarget; const href = link.getAttribute("href"); if (href?.startsWith("#")) { event.preventDefault(); const sectionId = href.slice(1); this.scrollTo(sectionId); if (this.options.updateHistory) { history.pushState(null, "", href); } } }; this.options = { navSelector: options.navSelector ?? "[data-ss-scrollspy-link], .scrollspy-link", containerId: options.containerId ?? "", threshold: options.threshold ?? 0.5, offset: options.offset ?? 0, activeClass: options.activeClass ?? "active", activeParentClass: options.activeParentClass ?? "active", smoothScroll: options.smoothScroll ?? true, onChange: options.onChange ?? (() => { }), updateHistory: options.updateHistory ?? false, }; this.scrollContainer = this.options.containerId ? (document.getElementById(this.options.containerId) ?? window) : window; this.init(); } static fromElements(sections, navLinksSelector, containerId, thresholdOffset = 0.5) { const instance = new ScrollSpyManager({ navSelector: navLinksSelector, containerId, threshold: thresholdOffset, }); instance.sections = sections; instance.updateActiveLink(); return instance; } refresh() { this.findSections(); this.updateActiveLink(); } scrollTo(sectionId) { const section = document.getElementById(sectionId); if (!section) return; const top = section.offsetTop - this.options.offset; if (this.scrollContainer instanceof Window) { window.scrollTo({ top, behavior: this.options.smoothScroll ? "smooth" : "auto", }); } else { this.scrollContainer.scrollTo({ top, behavior: this.options.smoothScroll ? "smooth" : "auto", }); } } getActive() { return this.currentActiveId; } destroy() { const container = this.scrollContainer instanceof Window ? window : this.scrollContainer; container.removeEventListener("scroll", this.handleScroll); this.navLinks.forEach((link) => { link.removeEventListener("click", this.handleLinkClick); }); this.sections = []; this.navLinks = []; } static init() { const managers = []; document .querySelectorAll('[data-ss="scrollspy"]') .forEach((el) => { const navSelector = el.dataset.ssScrollspyNav || `#${el.id} a`; const threshold = el.dataset.ssScrollspyThreshold; const smooth = el.dataset.ssScrollspySmooth !== "false"; const offset = el.dataset.ssScrollspyOffset; managers.push(new ScrollSpyManager({ navSelector, threshold: threshold ? parseFloat(threshold) : undefined, smoothScroll: smooth, offset: offset ? parseInt(offset, 10) : undefined, })); }); return managers; } init() { this.findSections(); this.bindScrollListener(); this.bindLinkListeners(); this.updateActiveLink(); } findSections() { this.navLinks = Array.from(document.querySelectorAll(this.options.navSelector)); this.sections = []; this.navLinks.forEach((link) => { const href = link.getAttribute("href"); if (href?.startsWith("#")) { const sectionId = href.slice(1); const section = document.getElementById(sectionId); if (section && !this.sections.includes(section)) { this.sections.push(section); } } }); } bindScrollListener() { const container = this.scrollContainer instanceof Window ? window : this.scrollContainer; container.addEventListener("scroll", this.handleScroll, { passive: true, }); } bindLinkListeners() { this.navLinks.forEach((link) => { link.addEventListener("click", this.handleLinkClick); }); } updateActiveLink() { if (this.sections.length === 0 || this.navLinks.length === 0) return; const scrollY = this.scrollContainer instanceof Window ? window.scrollY : this.scrollContainer.scrollTop; let activeId = null; for (const section of this.sections) { const id = section.getAttribute("id"); if (!id) continue; const top = section.offsetTop - this.options.offset; const height = section.offsetHeight; const threshold = top - height * this.options.threshold; if (scrollY >= threshold) { activeId = id; } } if (activeId === this.currentActiveId) return; this.currentActiveId = activeId; let activeLink = null; this.navLinks.forEach((link) => { const targetId = link.getAttribute("href")?.replace("#", ""); const isActive = targetId === activeId; link.classList.toggle(this.options.activeClass, isActive); link.setAttribute("aria-current", isActive ? "true" : "false"); if (isActive) { activeLink = link; } this.updateParentClasses(link, isActive); }); this.options.onChange(activeId, activeLink); } updateParentClasses(link, isActive) { let parent = link.parentElement; while (parent && parent !== document.body) { if (parent.tagName === "LI") { parent.classList.toggle(this.options.activeParentClass, isActive); } parent = parent.parentElement; } } } export default ScrollSpyManager; //# sourceMappingURL=ScrollSpyManager.js.map