stylescape
Version:
Stylescape is a visual identity framework developed by Scape Agency.
189 lines • 6.85 kB
JavaScript
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