Skip to content

Instantly share code, notes, and snippets.

@danharper
Created May 10, 2016 16:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save danharper/f456b2bb718346bae50796ca4d3d6eca to your computer and use it in GitHub Desktop.
Save danharper/f456b2bb718346bae50796ca4d3d6eca to your computer and use it in GitHub Desktop.
export type Options = {
blockSelector: () => string,
linkSelector: (id: string) => string,
classToToggle?: () => string,
toggleClass?: (el: Element, isInView: boolean, classToToggle: string) => void,
}
const defaults: Options = {
blockSelector: () => '',
linkSelector: () => '',
classToToggle: () => 'active',
toggleClass: (el: Element, isInView: boolean, classToToggle: string) => {
if (isInView) {
el.classList.add(classToToggle)
}
else {
el.classList.remove(classToToggle)
}
}
}
type Bounds = { top: number, bottom: number }
export default class ScrollSpy {
private options: Options
private elements: Array<Bounds & { link: Element }>
constructor(options: Options) {
this.options = Object.assign({}, defaults, options)
document.addEventListener('DOMContentLoaded', () => {
this.syncElementPositions()
this.bootWindowListeners()
this.highlightElementsInViewport()
})
}
private bootWindowListeners() {
window.addEventListener('resize', () => {
this.syncElementPositions()
this.highlightElementsInViewport()
})
window.addEventListener('scroll', this.highlightElementsInViewport.bind(this))
}
private syncElementPositions() {
this.elements = this.select(this.options.blockSelector()).map((el: Element) => {
return {
link: document.querySelector(this.options.linkSelector(el.id)),
top: el.getBoundingClientRect().top,
bottom: el.getBoundingClientRect().top + el.getBoundingClientRect().height,
}
})
}
private highlightElementsInViewport() {
const viewport: Bounds = {
top: window.scrollY,
bottom: window.scrollY + window.innerHeight,
}
this.elements.forEach(block => {
const isInView = this.isInViewport(viewport, block)
this.options.toggleClass(block.link, isInView, this.options.classToToggle())
})
}
private isInViewport({ top: vTop, bottom: vBot }: Bounds, { top: eTop, bottom: eBot }: Bounds) {
const bottomShowing = eTop <= vTop && eBot > vTop
const encased = eTop > vTop && eBot < vBot
const topShowing = eTop > vTop && eTop < vBot
const covering = eTop > vTop && eTop < vBot
return bottomShowing || encased || topShowing || covering
}
private select(selector: string): Element[] {
return Array.from(document.querySelectorAll(selector))
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment