Created
June 7, 2018 14:50
-
-
Save bassplayer7/e2eee572aef3d440c6dd0fd15a13cf9e to your computer and use it in GitHub Desktop.
Sticky Section Handler that maps links to sections during page scroll
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* @by SwiftOtter, Inc. 12/13/17 | |
* @website https://swiftotter.com | |
* | |
* Aspects: handles non-supporting browsers; integrates with smooth scrolling; maps parent links to related sections; with an | |
* optional attribute taking precedence if supplied; gracefully handles multiple sections per link if present; | |
* accommodates browser idiosyncrasies; disables link highlighting when link clicked and during scroll; | |
* adds class to navbar when stuck | |
**/ | |
define(['zenscroll'], function(zenscroll) { | |
let preventSwitching = false; | |
const supportsSticky = window.CSS && typeof CSS.supports === 'function' && CSS.supports('position', 'sticky'); | |
class StickySection { | |
constructor(section) { | |
this.section = section; | |
this.elements = section.querySelectorAll('a[href*="#"]'); | |
this.references = []; | |
this.visibleReferences = []; | |
this.barHeight = this.section.getBoundingClientRect().height; | |
if (typeof Stickyfill !== 'undefined') { | |
// Polyfill sticky | |
Stickyfill.addOne(section); | |
} | |
this.setupReferences(); | |
this.watchReferences(); | |
this.watchStickyBar(); | |
} | |
setupReferences() { | |
this.elements.forEach(element => { | |
const reference = this.getReferenceFor(element); | |
if (reference) { | |
this.references.push(reference); | |
this.addNavLinkToReference(reference, element); | |
element.addEventListener('click', this.handleLinkClick.bind(this, reference)); | |
} | |
}); | |
} | |
addNavLinkToReference(reference, element) { | |
if (!Array.isArray(reference.navLink)) { | |
reference.navLink = []; | |
} | |
reference.navLink.push(element); | |
} | |
getReferenceFor(element) { | |
let reference, | |
hashIndex = element.getAttribute('href').indexOf('#'); | |
if (element.className.indexOf('sticky__button') !== -1 || hashIndex === -1) { | |
return; | |
} | |
if (element.getAttribute('data-section')) { | |
reference = document.querySelector(element.getAttribute('data-section')); | |
} | |
if (!reference) { | |
reference = document.getElementById(element.getAttribute('href').substr(hashIndex + 1)); | |
} | |
return reference; | |
} | |
handleLinkClick(reference, e) { | |
preventSwitching = true; | |
e.stopPropagation(); | |
e.preventDefault(); | |
this.activateLink(reference.navLink); | |
zenscroll.toY(zenscroll.getTopOf(reference) - this.barHeight + 2, null, () => { | |
setTimeout(() => {preventSwitching = false}, 50); | |
}); | |
} | |
activateLink(link) { | |
this.elements.forEach(element => element.classList.remove('is-active')); | |
link.forEach(element => element.classList.add('is-active')); | |
if (navigator.userAgent.indexOf('Edge') !== -1 && supportsSticky) { | |
requestAnimationFrame(() => { | |
// Edge has terrible bugs with position: sticky. However because it so-called "supports" position sticky | |
// we can't use the polyfill for it. As a work around for the problem, this forces the | |
// sticky bar to repaint. While it causes a slight jank in the scroll bar at times on IE - especially when | |
// scrolling more quickly, this appears to decrease the problem where the scroll bar gets stuck | |
// part way down the page. | |
this.section.style.setProperty('top', '-1px'); | |
requestAnimationFrame(() => this.section.style.setProperty('top', '0')); | |
}); | |
} | |
} | |
watchStickyBar() { | |
const intersectionOptions = {rootMargin: `0px`, threshold: [1, 0]}; | |
const observer = new IntersectionObserver((entries) => { | |
entries.forEach(bar => { | |
const targetInfo = bar.boundingClientRect; | |
if (targetInfo.bottom >= bar.rootBounds.top && targetInfo.top < 1) { | |
bar.target.classList.add('is-stuck'); | |
} else { | |
bar.target.classList.remove('is-stuck'); | |
} | |
}); | |
}, intersectionOptions); | |
observer.observe(this.section); | |
} | |
watchReferences() { | |
const intersectionOptions = { | |
rootMargin: `-${this.barHeight}px 0px 0px 0px`, | |
threshold: [0, 0.25, 0.5, 0.75, 1] | |
}; | |
const observer = new IntersectionObserver(this.referencePositionChange.bind(this), intersectionOptions); | |
this.references.forEach(reference => observer.observe(reference)); | |
} | |
/** | |
* Handles entries from the IntersectionObserver and itself (recursive). Items that are onscreen are queued | |
* to be shown later if necessary. | |
* | |
* @param entries | |
*/ | |
referencePositionChange(entries) { | |
const visibleEntries = entries.filter(entry => entry.isIntersecting) | |
.sort((a, b) => a.boundingClientRect.bottom - b.boundingClientRect.bottom); | |
this.updateVisibleEntries(visibleEntries); | |
const firstVisible = visibleEntries.first(); | |
if (!preventSwitching && firstVisible && firstVisible.target.navLink && !this.referenceAlreadyVisible(firstVisible)) { | |
this.activateLink(firstVisible.target.navLink); | |
this.currentReference = firstVisible.target; | |
} | |
this.clearHiddenReferences(entries); | |
if (!firstVisible && this.visibleReferences && this.visibleReferences.length > 0) { | |
this.referencePositionChange(this.visibleReferences); | |
} | |
} | |
referenceAlreadyVisible(firstVisible) { | |
if (!this.currentReference) return false; | |
const currentRect = this.currentReference.getBoundingClientRect(); | |
return currentRect.top < firstVisible.boundingClientRect.top | |
&& currentRect.bottom > this.barHeight; | |
} | |
/** | |
* Manages sticky links but also triggers removals from the queued entries due to items being offscreen | |
* | |
* @param entries | |
*/ | |
clearHiddenReferences(entries) { | |
entries.filter(entry => !entry.isIntersecting) | |
.forEach(entry => { | |
if (entry.target.navLink) { | |
entry.target.navLink.forEach(link => link.classList.remove('is-active')); | |
} | |
this.removeReferencesFromVisibleArray(entry); | |
}); | |
} | |
removeReferencesFromVisibleArray(entry) { | |
let indexToRemove = this.visibleReferences.findIndex(visible => visible.target.isSameNode(entry.target)); | |
if (indexToRemove !== -1) { | |
this.visibleReferences.splice(indexToRemove, 1); | |
} | |
} | |
/** | |
* Updates queued entries with any new items that are visible | |
* | |
* @param visibleEntries | |
*/ | |
updateVisibleEntries(visibleEntries) { | |
if (!this.visibleReferences) { | |
this.visibleReferences = visibleEntries; | |
} else { | |
visibleEntries.forEach(visible => { | |
if (!this.visibleReferences.find(reference => visible.target.isSameNode(reference.target))) { | |
this.visibleReferences.push(visible); | |
} | |
}); | |
} | |
} | |
} | |
document.querySelectorAll('.sticky').forEach(sticky => new StickySection(sticky)); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment