00341252a1
The floating TOC positioning logic assumed a correct sidebar reference when calculating the horizontal offset. However, the selector used (`aside, .sidebar, #secondary`) could match non-layout elements such as hidden containers, mobile sidebars, or unrelated widgets. This resulted in incorrect `getBoundingClientRect()` values and caused the TOC to be positioned too far left or right, depending on which element was matched. Solution: - Introduced a `getRealSidebar()` helper to dynamically detect the visually relevant sidebar element. - Filters out non-visible or irrelevant elements based on size. - Selects the right-most valid candidate, ensuring correct layout context. - Uses the actual gap between content and sidebar to position the TOC symmetrically on the opposite side of the content. Additional improvements: - Cached sidebar lookup to avoid repeated DOM queries during scroll. - Stabilized gap calculation with clamping to prevent layout drift. Result: The TOC now consistently aligns with the content column and mirrors the sidebar spacing correctly across different layouts and breakpoints.
272 lines
7.9 KiB
JavaScript
272 lines
7.9 KiB
JavaScript
document.addEventListener('DOMContentLoaded', function () {
|
|
var toc = document.getElementById('zeitfresser-floating-toc');
|
|
var title = document.querySelector('.zeitfresser-article-heading .page-title, .zeitfresser-article-heading .entry-title, .entry-header .entry-title');
|
|
var progressBar = document.getElementById('zeitfresser-floating-toc-progress');
|
|
var nav = toc ? toc.querySelector('.zeitfresser-floating-toc__nav') : null;
|
|
|
|
if (!toc || !title) {
|
|
return;
|
|
}
|
|
|
|
var links = Array.prototype.slice.call(toc.querySelectorAll('a[data-target]'));
|
|
var desktopQuery = window.matchMedia('(min-width: 1500px)');
|
|
var stickyTop = 100;
|
|
var headingOffset = 88;
|
|
var ticking = false;
|
|
|
|
var tocBottomOffset = null;
|
|
|
|
function isDesktop() {
|
|
return desktopQuery.matches;
|
|
}
|
|
|
|
function getTarget(link) {
|
|
var id = link.getAttribute('data-target');
|
|
return id ? document.getElementById(id) : null;
|
|
}
|
|
|
|
function getHeadings() {
|
|
return links.map(function (link) {
|
|
return {
|
|
link: link,
|
|
target: getTarget(link)
|
|
};
|
|
}).filter(function (item) {
|
|
return !!item.target;
|
|
});
|
|
}
|
|
|
|
function getArticleElement() {
|
|
return document.querySelector(
|
|
'.single-post .post-content article, ' +
|
|
'.single-post .post-content, ' +
|
|
'article.post, article, ' +
|
|
'.entry-content'
|
|
);
|
|
}
|
|
|
|
function getTocBottomOffset() {
|
|
if (tocBottomOffset !== null) return tocBottomOffset;
|
|
|
|
var value = getComputedStyle(document.documentElement)
|
|
.getPropertyValue('--toc-bottom-offset')
|
|
.trim();
|
|
|
|
tocBottomOffset = parseInt(value, 10) || 12;
|
|
return tocBottomOffset;
|
|
}
|
|
|
|
let cachedSidebar = null;
|
|
|
|
function getRealSidebar() {
|
|
if (cachedSidebar) return cachedSidebar;
|
|
|
|
var candidates = Array.from(document.querySelectorAll('aside, .sidebar, #secondary'));
|
|
|
|
if (!candidates.length) return null;
|
|
|
|
cachedSidebar = candidates
|
|
.map(el => ({
|
|
el,
|
|
rect: el.getBoundingClientRect()
|
|
}))
|
|
.filter(item => item.rect.width > 200 && item.rect.height > 200)
|
|
.sort((a, b) => b.rect.left - a.rect.left)[0]?.el || null;
|
|
|
|
return cachedSidebar;
|
|
}
|
|
|
|
function syncPosition() {
|
|
if (!isDesktop()) {
|
|
document.documentElement.style.setProperty('--zeitfresser-toc-top', stickyTop + 'px');
|
|
document.documentElement.style.setProperty('--zeitfresser-toc-left', '24px');
|
|
document.documentElement.style.setProperty('--zeitfresser-toc-width', '220px');
|
|
return;
|
|
}
|
|
|
|
var scrollTop = window.scrollY || window.pageYOffset || 0;
|
|
var titleRect = title.getBoundingClientRect();
|
|
|
|
var contentColumn =
|
|
document.querySelector('.inside-page .main-wrapper > section') ||
|
|
document.querySelector('#primary') ||
|
|
document.querySelector('.content-area') ||
|
|
title;
|
|
|
|
var sidebar = getRealSidebar();
|
|
|
|
if (!contentColumn || !sidebar) return;
|
|
|
|
var contentRect = contentColumn.getBoundingClientRect();
|
|
var sidebarRect = sidebar.getBoundingClientRect();
|
|
|
|
// --- stable gap ---
|
|
var gap = sidebarRect.left - contentRect.right;
|
|
|
|
if (gap < 0) {
|
|
gap = 48;
|
|
}
|
|
|
|
gap = Math.max(32, Math.min(gap, 120));
|
|
|
|
var tocWidth = 220;
|
|
|
|
// --- final position ---
|
|
var tocLeft = Math.round(
|
|
contentRect.left - gap - tocWidth
|
|
);
|
|
|
|
tocLeft = Math.max(24, tocLeft);
|
|
|
|
var tocTop = Math.max(
|
|
stickyTop,
|
|
Math.round(titleRect.top + scrollTop + 14)
|
|
);
|
|
|
|
document.documentElement.style.setProperty('--zeitfresser-toc-top', tocTop + 'px');
|
|
document.documentElement.style.setProperty('--zeitfresser-toc-left', tocLeft + 'px');
|
|
document.documentElement.style.setProperty('--zeitfresser-toc-width', tocWidth + 'px');
|
|
}
|
|
|
|
function handleFooterCollision() {
|
|
if (!isDesktop()) {
|
|
toc.style.transform = '';
|
|
return;
|
|
}
|
|
|
|
var article = getArticleElement();
|
|
|
|
if (!article) {
|
|
toc.style.transform = '';
|
|
return;
|
|
}
|
|
|
|
toc.style.transform = '';
|
|
|
|
var scrollTop = window.scrollY || window.pageYOffset;
|
|
|
|
var articleRect = article.getBoundingClientRect();
|
|
var articleBottom = articleRect.top + scrollTop + articleRect.height;
|
|
|
|
var tocRect = toc.getBoundingClientRect();
|
|
var tocTop = tocRect.top + scrollTop;
|
|
var tocHeight = tocRect.height;
|
|
var tocBottom = tocTop + tocHeight;
|
|
|
|
var offset = getTocBottomOffset();
|
|
|
|
var maxBottom = articleBottom - offset;
|
|
var overflow = Math.ceil(tocBottom - maxBottom);
|
|
|
|
if (overflow > 0) {
|
|
toc.style.transform = 'translateY(-' + overflow + 'px)';
|
|
}
|
|
}
|
|
|
|
function setActiveLink(id) {
|
|
links.forEach(function (link) {
|
|
var active = link.getAttribute('data-target') === id;
|
|
link.classList.toggle('is-active', active);
|
|
|
|
if (active) {
|
|
link.setAttribute('aria-current', 'true');
|
|
} else {
|
|
link.removeAttribute('aria-current');
|
|
}
|
|
});
|
|
}
|
|
|
|
function updateProgress() {
|
|
if (!progressBar) return;
|
|
|
|
var article = getArticleElement();
|
|
|
|
if (!article) {
|
|
progressBar.style.width = '0%';
|
|
return;
|
|
}
|
|
|
|
var rect = article.getBoundingClientRect();
|
|
var total = Math.max(article.offsetHeight - window.innerHeight, 1);
|
|
var progress = Math.min(Math.max((-rect.top / total) * 100, 0), 100);
|
|
|
|
progressBar.style.width = progress + '%';
|
|
}
|
|
|
|
function updateActiveHeading() {
|
|
var headings = getHeadings();
|
|
if (!headings.length) return;
|
|
|
|
var currentId = headings[0].target.id;
|
|
var triggerY = headingOffset + 24;
|
|
|
|
headings.forEach(function (item) {
|
|
if (item.target.getBoundingClientRect().top <= triggerY) {
|
|
currentId = item.target.id;
|
|
}
|
|
});
|
|
|
|
setActiveLink(currentId);
|
|
}
|
|
|
|
function onViewportChange() {
|
|
if (ticking) return;
|
|
|
|
ticking = true;
|
|
|
|
window.requestAnimationFrame(function () {
|
|
syncPosition();
|
|
handleFooterCollision();
|
|
updateProgress();
|
|
updateActiveHeading();
|
|
ticking = false;
|
|
});
|
|
}
|
|
|
|
links.forEach(function (link) {
|
|
link.addEventListener('click', function (event) {
|
|
var target = getTarget(link);
|
|
if (!target) return;
|
|
|
|
event.preventDefault();
|
|
|
|
var top = target.getBoundingClientRect().top + window.scrollY - headingOffset;
|
|
|
|
window.scrollTo({
|
|
top: top,
|
|
behavior: 'smooth'
|
|
});
|
|
|
|
setActiveLink(target.id);
|
|
});
|
|
});
|
|
|
|
if (nav) {
|
|
nav.addEventListener('wheel', function (event) {
|
|
var canScroll = nav.scrollHeight > nav.clientHeight;
|
|
if (!canScroll) return;
|
|
|
|
var atTop = nav.scrollTop <= 0;
|
|
var atBottom = Math.ceil(nav.scrollTop + nav.clientHeight) >= nav.scrollHeight;
|
|
|
|
if ((event.deltaY < 0 && !atTop) || (event.deltaY > 0 && !atBottom)) {
|
|
event.preventDefault();
|
|
nav.scrollTop += event.deltaY;
|
|
}
|
|
}, { passive: false });
|
|
}
|
|
|
|
// Initial run
|
|
syncPosition();
|
|
handleFooterCollision();
|
|
updateProgress();
|
|
updateActiveHeading();
|
|
|
|
requestAnimationFrame(function () {
|
|
toc.classList.add('is-visible');
|
|
});
|
|
|
|
window.addEventListener('scroll', onViewportChange, { passive: true });
|
|
window.addEventListener('resize', onViewportChange, { passive: true });
|
|
});
|