Skip to content
This repository has been archived by the owner on Feb 16, 2024. It is now read-only.

feat: adjusts scrollspy so it observes intersecting of headlines #57

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
4 changes: 2 additions & 2 deletions src/_layouts/kurzbericht.11ty.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ module.exports = {
const status = item.data.meta && item.data.meta.status ? `is-${item.data.meta.status}` : '';

return `
<section class="${status} ${item.data.class ? item.data.class : ''} ${item.data.level===1 ? 'has-seperator' : ''}" id="${this.slugify(item.data.title)}" data-js-scrollspy-section>
<section class="${status} ${item.data.class ? item.data.class : ''} ${item.data.level===1 ? 'has-seperator' : ''}">
<div class="content">
<h${item.data.level + 1}>${item.data.title}
<h${item.data.level + 1} id="${this.slugify(item.data.title)}">${item.data.title}
${utils.getOpenInNewWindowLink(item, data)}${utils.getEditLink(item, data)}</h${item.data.level + 1}>
${meta}
${item.content}
Expand Down
122 changes: 44 additions & 78 deletions src/assets/scripts/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -230,8 +230,7 @@ const addScrollToTop = () => {

/* Scrollspy
############################################################################ */

const addScrollSpy = () => {
const addScrollSpy = () => {
const inlineNavigation = document.querySelector("[data-js-scrollspy]");
if(!inlineNavigation) return;

Expand All @@ -243,94 +242,61 @@ const addScrollSpy = () => {
}
if(viewportSize !== "large") return;

const sections = document.querySelectorAll("[data-js-scrollspy-section]");
const headlines = document.querySelectorAll('h2[id], h3[id]');
const arrayHeadlines = Array.from(headlines);
let lastActiveElementId = false;
const intersectionCallback = (entries, observer) => {
let updated = false;
entries.forEach(entry => {
// if the menu was aleady updated by a previous entry, we don't need to check the other entries.
if (updated) {
return;
}
let activeBlock; // the block that should be highlighted in the menu.
if(!entry.isIntersecting) {

// This element has just left the viewport. This means at least one of 2 possibilities:
// 1. the next element has its top aligned with the top of the viewport.
// In this case the next element should be activated.
// 2. the previous element is (at least partly) visible.
// In this case we need to keep looking at previous elements until
// we find the highest one that is still visible.
if (isInViewport(entry.target.nextElementSibling)) {
// possibility 1.
activeBlock = entry.target.nextElementSibling;

} else if (isInViewport(entry.target.previousElementSibling)) {
// possibility 2.
activeBlock = entry.target.previousElementSibling;
while ( isInViewport(activeBlock.previousElementSibling) ) {
activeBlock = activeBlock.previousElementSibling;
}
}
} else {
// This element just entered the viewport. 2 possibilities:
// 1. The top of the element moved in from the bottom.
// In this case nothing needs to happen.
// 2. The bottom of the element moved in from the top.
// In this case the current element should also become active.
if (!isInViewport(entry.target.nextElementSibling)) {
// possibility 1.
return;

} else if (!isInViewport(entry.target.previousElementSibling)) {
// possibility 2.
activeBlock = entry.target;
if (entry.isIntersecting) {
let position = (entry.boundingClientRect.top + (entry.boundingClientRect.top - entry.boundingClientRect.bottom));
if (position < 0) {
if (arrayHeadlines.indexOf(entry.target) > 0) {
if (lastActiveElementId) {
setStatusNavElement(lastActiveElementId, false);
}
const previousElement = arrayHeadlines[arrayHeadlines.indexOf(entry.target) - 1];
setStatusNavElement(previousElement, true);
lastActiveElementId = previousElement;
}
}
} else {
if (entry.intersectionRatio < 0) {
} else {
if (entry.boundingClientRect.top < 1) {
if (lastActiveElementId) {
setStatusNavElement(lastActiveElementId, false);
}
setStatusNavElement(entry.target, true);
lastActiveElementId = entry.target;
}
}
}
}

// remove active class on all elements.
inlineNavigation.querySelectorAll('.is-active').forEach((item) => {
item.classList.remove('is-active');
});
// Couldn't deternmine an active block. So don't do anything.
if (!activeBlock) {
return;
}

// get id of the intersecting section
const id = activeBlock.getAttribute('id');

// find matching link and add appropriate class
const navElement = inlineNavigation.querySelector(`[data-scrollspy-target="${id}"] a`);

if(!navElement) return;
navElement.classList.add('is-active');

// Wer're done. Set flag to make sure that we don't check other entries.
updated = true;
});
};


const isInViewport = (element) => {
if (!element) {
return false;
}
const rect = element.getBoundingClientRect();
return (
rect.bottom >= 0 &&
rect.top <= (window.innerHeight || document.documentElement.clientHeight)
);
}

const intersectionObserver = new IntersectionObserver(intersectionCallback, {
rootMargin: '-1px',
rootMargin: '-1px 0px 0px 0px',
threshold: [1],
});

sections.forEach((section) => {
intersectionObserver.observe(section);
headlines.forEach((headline) => {
intersectionObserver.observe(headline);
});

const setStatusNavElement = (target, isActive) => {
const id = target.getAttribute('id');
if(!id) return;
const navElement = inlineNavigation.querySelector(`[data-scrollspy-target="${id}"] a`);
if(!navElement) return;
if(isActive) {
navElement.classList.add('is-active');
} else {
navElement.classList.remove('is-active');
}
}
};


/* Größe des Viewports ermitteln
############################################################################ */

Expand Down