Skip to content

Instantly share code, notes, and snippets.

@bassplayer7
Created June 7, 2018 14:50
Show Gist options
  • Save bassplayer7/e2eee572aef3d440c6dd0fd15a13cf9e to your computer and use it in GitHub Desktop.
Save bassplayer7/e2eee572aef3d440c6dd0fd15a13cf9e to your computer and use it in GitHub Desktop.
Sticky Section Handler that maps links to sections during page scroll
/**
* @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