From 6bf38ae05d240f8586aa6662e719d757bf0e977c Mon Sep 17 00:00:00 2001 From: Dome Date: Sun, 26 Apr 2026 18:48:28 +0200 Subject: [PATCH] refactor(toc, customizer): improve TOC architecture and reorganize customizer settings - Reorganized Customizer structure for improved clarity and maintainability - Introduced consistent default values for all settings to ensure stable fallbacks when no user preferences are defined - Refactored scroll-driven TOC implementation: - Optimized scroll handling using requestAnimationFrame - Reduced layout thrashing and unnecessary DOM reads - Improved heading detection logic (deterministic viewport trigger) - Enhanced positioning logic (responsive alignment + sidebar awareness) - Improved footer collision handling for more robust layout behavior - Added optional IntersectionObserver-based TOC implementation: - Event-driven alternative to scroll-based approach - Currently not enabled by default - May not be supported long-term due to less deterministic behavior - General cleanup and internal consistency improvements chore: bump version to 2.4.0 --- .../js/toc (Scroll-Driven Implementation).js | 322 ++++++++++++++++++ assets/js/toc (observer).js | 299 ++++++++++++++++ assets/js/toc.js | 125 +++++-- inc/customizer/general.php | 114 +++++-- inc/customizer/layout.php | 4 +- inc/customizer/social.php | 44 ++- inc/customizer/toc.php | 45 ++- style.css | 2 +- 8 files changed, 864 insertions(+), 91 deletions(-) create mode 100644 assets/js/toc (Scroll-Driven Implementation).js create mode 100644 assets/js/toc (observer).js diff --git a/assets/js/toc (Scroll-Driven Implementation).js b/assets/js/toc (Scroll-Driven Implementation).js new file mode 100644 index 0000000..caf33b3 --- /dev/null +++ b/assets/js/toc (Scroll-Driven Implementation).js @@ -0,0 +1,322 @@ +/** + * Floating TOC (Scroll-Driven Implementation) + * + * This implementation uses a scroll-based approach combined with + * requestAnimationFrame to determine the currently active heading. + * The active section is calculated based on a fixed viewport trigger + * (offset from the top), ensuring predictable and stable behavior. + * + * Design Goals: + * - Deterministic highlighting (no competing states) + * - Visual stability (no flickering or race conditions) + * - Theme compatibility (no reliance on experimental APIs) + * - Maintainable and debuggable logic + * + * Characteristics: + * - Uses a cached list of headings for efficient iteration + * - Throttles scroll handling via requestAnimationFrame + * - Minimizes DOM writes and layout recalculations + * - Separates layout (positioning) from state (active link) + * + * Trade-offs: + * - Runs continuously during scroll (minor CPU overhead) + * - Relies on getBoundingClientRect for visibility detection + * - Less "event-driven" than IntersectionObserver-based approaches + * + * Notes: + * This approach was chosen over IntersectionObserver to guarantee + * consistent ordering, eliminate flickering edge cases, and provide + * fully deterministic behavior across all layouts and browsers. + */ + +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; + var cachedSidebar = null; + var headings = getHeadings(); + + 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; + } + + function getRealSidebar() { + if (cachedSidebar) return cachedSidebar; + + var candidates = Array.prototype.slice.call( + document.querySelectorAll('aside, .sidebar, #secondary') + ); + + cachedSidebar = candidates + .filter(function (el) { + var rect = el.getBoundingClientRect(); + return rect.width > 200 && rect.height > 200; + }) + .sort(function (a, b) { + var rectA = a.getBoundingClientRect(); + var rectB = b.getBoundingClientRect(); + return rectB.left - rectA.left; + })[0] || 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(); + + // 🔥 bessere Content-Erkennung + var contentColumn = + document.querySelector('.inside-page .main-wrapper > section') || + document.querySelector('#primary') || + document.querySelector('.content-area') || + title; + + if (!contentColumn) return; + + var sidebar = getRealSidebar(); + var contentRect = contentColumn.getBoundingClientRect(); + var sidebarRect = sidebar ? sidebar.getBoundingClientRect() : null; + + var gap = 48; + + if (sidebarRect) { + gap = Math.abs(sidebarRect.left - contentRect.right); + gap = Math.max(32, Math.min(gap, 120)); + } + + var maxWidth = Math.max(Math.round(contentRect.left - gap - 24), 180); + var tocWidth = Math.max(220, Math.min(260, maxWidth)); + + var tocLeft = Math.max( + 24, + Math.round(contentRect.left - gap - tocWidth) + ); + + 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() { + if (!headings.length) return; + + var currentId = headings[0].target.id; + var triggerY = headingOffset + 24; + + for (var i = 0; i < headings.length; i++) { + var rectTop = headings[i].target.getBoundingClientRect().top; + + if (rectTop <= triggerY) { + currentId = headings[i].target.id; + } else { + break; + } + } + + 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 }); + window.addEventListener('load', function () { + syncPosition(); + }); + +}); diff --git a/assets/js/toc (observer).js b/assets/js/toc (observer).js new file mode 100644 index 0000000..8968825 --- /dev/null +++ b/assets/js/toc (observer).js @@ -0,0 +1,299 @@ +/** + * Floating TOC (IntersectionObserver Version) + * + * This implementation relies on the IntersectionObserver API to detect which + * headings are currently visible in the viewport. It is event-driven and + * more efficient, as updates occur only when visibility changes. + * + * Pros: + * - Lower CPU usage (no continuous polling) + * - Native browser optimization + * + * Cons: + * - Can produce multiple competing active states + * - May require tuning to avoid flickering or reordering + */ + +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 cachedSidebar = null; + 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; + } + + function getRealSidebar() { + if (cachedSidebar) return cachedSidebar; + + var candidates = Array.prototype.slice.call( + document.querySelectorAll('aside, .sidebar, #secondary') + ); + + cachedSidebar = candidates + .filter(function (el) { + var rect = el.getBoundingClientRect(); + return rect.width > 200 && rect.height > 200; + }) + .sort(function (a, b) { + return b.getBoundingClientRect().left - a.getBoundingClientRect().left; + })[0] || 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(); + + // 🔥 bessere Content-Erkennung + var contentColumn = + document.querySelector('.inside-page .main-wrapper > section') || + document.querySelector('#primary') || + document.querySelector('.content-area') || + title; + + if (!contentColumn) return; + + var sidebar = getRealSidebar(); + + var contentRect = contentColumn.getBoundingClientRect(); + var sidebarRect = sidebar ? sidebar.getBoundingClientRect() : null; + + var gap = 48; + + if (sidebarRect) { + gap = Math.abs(sidebarRect.left - contentRect.right); + gap = Math.max(32, Math.min(gap, 120)); + } + + // Toc Content Breite + var maxWidth = Math.max(Math.round(contentRect.left - gap - 24), 180); + var tocWidth = Math.max(220, Math.min(260, maxWidth)); + + var tocLeft = Math.max( + 24, + Math.round(contentRect.left - gap - tocWidth) + ); + + 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 onViewportChange() { + if (ticking) return; + + ticking = true; + + window.requestAnimationFrame(function () { + syncPosition(); + handleFooterCollision(); + updateProgress(); + ticking = false; + }); + } + + // 🔥 NEW: IntersectionObserver for active headings + let currentActiveId = null; + + function initIntersectionObserver() { + const headings = getHeadings(); + if (!headings.length) return; + + const observer = new IntersectionObserver(function (entries) { + + entries.forEach(function (entry) { + if (entry.isIntersecting) { + const id = entry.target.id; + + if (id !== currentActiveId) { + currentActiveId = id; + setActiveLink(id); + } + } + }); + + }, { + root: null, + rootMargin: `-${headingOffset}px 0px -70% 0px`, + threshold: 0 + }); + + headings.forEach(function (item) { + if (item.target) { + observer.observe(item.target); + } + }); + } + + 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' + }); + }); + }); + + 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(); + initIntersectionObserver(); + + requestAnimationFrame(function () { + toc.classList.add('is-visible'); + }); + + window.addEventListener('scroll', onViewportChange, { passive: true }); + window.addEventListener('resize', onViewportChange, { passive: true }); +}); diff --git a/assets/js/toc.js b/assets/js/toc.js index ae04466..caf33b3 100644 --- a/assets/js/toc.js +++ b/assets/js/toc.js @@ -1,6 +1,41 @@ +/** + * Floating TOC (Scroll-Driven Implementation) + * + * This implementation uses a scroll-based approach combined with + * requestAnimationFrame to determine the currently active heading. + * The active section is calculated based on a fixed viewport trigger + * (offset from the top), ensuring predictable and stable behavior. + * + * Design Goals: + * - Deterministic highlighting (no competing states) + * - Visual stability (no flickering or race conditions) + * - Theme compatibility (no reliance on experimental APIs) + * - Maintainable and debuggable logic + * + * Characteristics: + * - Uses a cached list of headings for efficient iteration + * - Throttles scroll handling via requestAnimationFrame + * - Minimizes DOM writes and layout recalculations + * - Separates layout (positioning) from state (active link) + * + * Trade-offs: + * - Runs continuously during scroll (minor CPU overhead) + * - Relies on getBoundingClientRect for visibility detection + * - Less "event-driven" than IntersectionObserver-based approaches + * + * Notes: + * This approach was chosen over IntersectionObserver to guarantee + * consistent ordering, eliminate flickering edge cases, and provide + * fully deterministic behavior across all layouts and browsers. + */ + 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 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; @@ -13,8 +48,9 @@ document.addEventListener('DOMContentLoaded', function () { var stickyTop = 100; var headingOffset = 88; var ticking = false; - var cachedSidebar = null; var tocBottomOffset = null; + var cachedSidebar = null; + var headings = getHeadings(); function isDesktop() { return desktopQuery.matches; @@ -26,14 +62,13 @@ document.addEventListener('DOMContentLoaded', function () { } function getHeadings() { - return links.map(function (link) { - return { - link: link, - target: getTarget(link) - }; - }).filter(function (item) { - return !!item.target; - }); + return links + .map(function (link) { + return { link: link, target: getTarget(link) }; + }) + .filter(function (item) { + return !!item.target; + }); } function getArticleElement() { @@ -55,7 +90,7 @@ document.addEventListener('DOMContentLoaded', function () { tocBottomOffset = parseInt(value, 10) || 12; return tocBottomOffset; } - + function getRealSidebar() { if (cachedSidebar) return cachedSidebar; @@ -69,7 +104,9 @@ document.addEventListener('DOMContentLoaded', function () { return rect.width > 200 && rect.height > 200; }) .sort(function (a, b) { - return b.getBoundingClientRect().left - a.getBoundingClientRect().left; + var rectA = a.getBoundingClientRect(); + var rectB = b.getBoundingClientRect(); + return rectB.left - rectA.left; })[0] || null; return cachedSidebar; @@ -96,7 +133,6 @@ document.addEventListener('DOMContentLoaded', function () { if (!contentColumn) return; var sidebar = getRealSidebar(); - var contentRect = contentColumn.getBoundingClientRect(); var sidebarRect = sidebar ? sidebar.getBoundingClientRect() : null; @@ -107,10 +143,9 @@ document.addEventListener('DOMContentLoaded', function () { gap = Math.max(32, Math.min(gap, 120)); } - // Toc Content Breite var maxWidth = Math.max(Math.round(contentRect.left - gap - 24), 180); var tocWidth = Math.max(220, Math.min(260, maxWidth)); - + var tocLeft = Math.max( 24, Math.round(contentRect.left - gap - tocWidth) @@ -133,7 +168,6 @@ document.addEventListener('DOMContentLoaded', function () { } var article = getArticleElement(); - if (!article) { toc.style.transform = ''; return; @@ -142,7 +176,6 @@ document.addEventListener('DOMContentLoaded', function () { toc.style.transform = ''; var scrollTop = window.scrollY || window.pageYOffset; - var articleRect = article.getBoundingClientRect(); var articleBottom = articleRect.top + scrollTop + articleRect.height; @@ -152,7 +185,6 @@ document.addEventListener('DOMContentLoaded', function () { var tocBottom = tocTop + tocHeight; var offset = getTocBottomOffset(); - var maxBottom = articleBottom - offset; var overflow = Math.ceil(tocBottom - maxBottom); @@ -178,7 +210,6 @@ document.addEventListener('DOMContentLoaded', function () { if (!progressBar) return; var article = getArticleElement(); - if (!article) { progressBar.style.width = '0%'; return; @@ -186,23 +217,30 @@ document.addEventListener('DOMContentLoaded', function () { 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); + + 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; + for (var i = 0; i < headings.length; i++) { + var rectTop = headings[i].target.getBoundingClientRect().top; + + if (rectTop <= triggerY) { + currentId = headings[i].target.id; + } else { + break; } - }); + } setActiveLink(currentId); } @@ -228,7 +266,10 @@ document.addEventListener('DOMContentLoaded', function () { event.preventDefault(); - var top = target.getBoundingClientRect().top + window.scrollY - headingOffset; + var top = + target.getBoundingClientRect().top + + window.scrollY - + headingOffset; window.scrollTo({ top: top, @@ -240,18 +281,26 @@ document.addEventListener('DOMContentLoaded', function () { }); if (nav) { - nav.addEventListener('wheel', function (event) { - var canScroll = nav.scrollHeight > nav.clientHeight; - if (!canScroll) return; + 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; + 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 }); + if ( + (event.deltaY < 0 && !atTop) || + (event.deltaY > 0 && !atBottom) + ) { + event.preventDefault(); + nav.scrollTop += event.deltaY; + } + }, + { passive: false } + ); } // Initial run @@ -266,4 +315,8 @@ document.addEventListener('DOMContentLoaded', function () { window.addEventListener('scroll', onViewportChange, { passive: true }); window.addEventListener('resize', onViewportChange, { passive: true }); + window.addEventListener('load', function () { + syncPosition(); + }); + }); diff --git a/inc/customizer/general.php b/inc/customizer/general.php index 310ff5b..71ac11b 100644 --- a/inc/customizer/general.php +++ b/inc/customizer/general.php @@ -10,7 +10,24 @@ add_action( 'customize_register', 'zeitfresser_general_options' ); function zeitfresser_general_options( $wp_customize ) { /** - * General Section (falls nicht schon vorhanden) + * 🔥 Custom Heading Control + */ + if ( class_exists( 'WP_Customize_Control' ) ) { + class ZTFR_Customize_Heading_Control extends WP_Customize_Control { + public $type = 'ztfr-heading'; + + public function render_content() { + ?> + + label ); ?> + + get_section( 'ztfr_general' ) ) { $wp_customize->add_section( @@ -23,28 +40,23 @@ function zeitfresser_general_options( $wp_customize ) { } /** - * Excerpt Length + * ------------------------ + * HEADER + * ------------------------ */ - $wp_customize->add_setting( - 'post_snippet_excerpt_size', - array( - 'default' => 20, - 'sanitize_callback' => 'absint', - ) - ); + $wp_customize->add_setting( 'ztfr_header_heading', array( + 'sanitize_callback' => 'sanitize_text_field', + )); $wp_customize->add_control( - 'post_snippet_excerpt_size', - array( - 'type' => 'number', - 'section' => 'ztfr_general', - 'label' => 'Excerpt Length (Post Cards)', - 'description' => 'Number of words shown in post previews.', - 'input_attrs' => array( - 'min' => 5, - 'max' => 100, - 'step' => 1, - ), + new ZTFR_Customize_Heading_Control( + $wp_customize, + 'ztfr_header_heading', + array( + 'label' => 'Header', + 'section' => 'ztfr_general', + 'priority' => 1, + ) ) ); @@ -62,9 +74,10 @@ function zeitfresser_general_options( $wp_customize ) { $wp_customize->add_control( 'show_hide_site_title', array( - 'type' => 'checkbox', - 'section' => 'ztfr_general', - 'label' => 'Show Site Title', + 'type' => 'checkbox', + 'section' => 'ztfr_general', + 'label' => 'Show Site Title', + 'priority' => 2, ) ); @@ -82,9 +95,58 @@ function zeitfresser_general_options( $wp_customize ) { $wp_customize->add_control( 'show_hide_site_tagline', array( - 'type' => 'checkbox', - 'section' => 'ztfr_general', - 'label' => 'Show Tagline', + 'type' => 'checkbox', + 'section' => 'ztfr_general', + 'label' => 'Show Tagline', + 'priority' => 3, + ) + ); + + /** + * ------------------------ + * GRID + * ------------------------ + */ + $wp_customize->add_setting( 'ztfr_grid_heading', array( + 'sanitize_callback' => 'sanitize_text_field', + )); + + $wp_customize->add_control( + new ZTFR_Customize_Heading_Control( + $wp_customize, + 'ztfr_grid_heading', + array( + 'label' => 'Grid', + 'section' => 'ztfr_general', + 'priority' => 20, + ) + ) + ); + + /** + * Excerpt Length + */ + $wp_customize->add_setting( + 'post_snippet_excerpt_size', + array( + 'default' => 25, + 'sanitize_callback' => 'absint', + ) + ); + + $wp_customize->add_control( + 'post_snippet_excerpt_size', + array( + 'type' => 'number', + 'section' => 'ztfr_general', + 'label' => 'Excerpt Length (Post Cards)', + 'description' => 'Number of words shown in post previews.', + 'priority' => 21, + 'input_attrs' => array( + 'min' => 5, + 'max' => 100, + 'step' => 1, + ), ) ); } diff --git a/inc/customizer/layout.php b/inc/customizer/layout.php index 93e2c65..a44934e 100644 --- a/inc/customizer/layout.php +++ b/inc/customizer/layout.php @@ -27,7 +27,7 @@ function zeitfresser_layout_options( $wp_customize ) { 'section' => 'ztfr_general', 'label' => esc_html__( 'Container Width', 'zeitfresser' ), 'description' => esc_html__( 'Maximum width of the content container in pixels.', 'zeitfresser' ), - 'priority' => 10, + 'priority' => 22, 'input_attrs' => array( 'min' => 800, 'max' => 2000, @@ -47,7 +47,7 @@ function zeitfresser_container_width_dynamic_css() { $container_width = (int) get_theme_mod( 'container_width' ); if ( $container_width <= 0 ) { - $container_width = 1140; + $container_width = 1400; } echo ''; diff --git a/inc/customizer/social.php b/inc/customizer/social.php index 887d745..f5b383b 100644 --- a/inc/customizer/social.php +++ b/inc/customizer/social.php @@ -6,11 +6,6 @@ */ if ( ! function_exists( 'zeitfresser_get_social_links' ) ) { - /** - * Return supported social networks. - * - * @return array - */ function zeitfresser_get_social_links() { return array( 'facebook' => esc_html__( 'Facebook', 'zeitfresser' ), @@ -31,31 +26,52 @@ add_action( 'customize_register', 'zeitfresser_social_links' ); function zeitfresser_social_links( $wp_customize ) { - $social_links = zeitfresser_get_social_links(); + /** + * 🔥 Heading Control (falls noch nicht vorhanden) + */ + if ( class_exists( 'WP_Customize_Control' ) && ! class_exists( 'ZTFR_Customize_Heading_Control' ) ) { + class ZTFR_Customize_Heading_Control extends WP_Customize_Control { + public $type = 'ztfr-heading'; + + public function render_content() { + ?> + + label ); ?> + + add_setting( 'ztfr_social_heading', array( - 'sanitize_callback' => 'wp_kses_post', + 'sanitize_callback' => 'sanitize_text_field', ) ); $wp_customize->add_control( - 'ztfr_social_heading', - array( - 'section' => 'ztfr_general', - 'type' => 'hidden', - 'description' => '
' . esc_html__( 'Social Links', 'zeitfresser' ) . '', - 'priority' => 30, + new ZTFR_Customize_Heading_Control( + $wp_customize, + 'ztfr_social_heading', + array( + 'label' => esc_html__( 'Social Links', 'zeitfresser' ), + 'section' => 'ztfr_general', + 'priority' => 30, + ) ) ); /** * Social URLs */ + $social_links = zeitfresser_get_social_links(); + $priority = 31; foreach ( $social_links as $key => $label ) { diff --git a/inc/customizer/toc.php b/inc/customizer/toc.php index 38f6ec2..984eb4b 100644 --- a/inc/customizer/toc.php +++ b/inc/customizer/toc.php @@ -10,27 +10,48 @@ add_action( 'customize_register', 'zeitfresser_toc_options' ); function zeitfresser_toc_options( $wp_customize ) { /** - * Section Divider (UI only) + * 🔥 Heading Control (falls noch nicht vorhanden) + */ + if ( class_exists( 'WP_Customize_Control' ) && ! class_exists( 'ZTFR_Customize_Heading_Control' ) ) { + class ZTFR_Customize_Heading_Control extends WP_Customize_Control { + public $type = 'ztfr-heading'; + + public function render_content() { + ?> + + label ); ?> + + add_setting( 'ztfr_toc_heading', array( - 'sanitize_callback' => 'wp_kses_post', + 'sanitize_callback' => 'sanitize_text_field', ) ); $wp_customize->add_control( - 'ztfr_toc_heading', - array( - 'section' => 'ztfr_general', - 'type' => 'hidden', - 'description' => '
' . esc_html__( 'Article TOC', 'zeitfresser' ) . '', - 'priority' => 20, + new ZTFR_Customize_Heading_Control( + $wp_customize, + 'ztfr_toc_heading', + array( + 'label' => esc_html__( 'TOC', 'zeitfresser' ), + 'section' => 'ztfr_general', + 'priority' => 10, + ) ) ); /** - * Toggle TOC + * Show TOC */ $wp_customize->add_setting( 'show_article_toc', @@ -47,7 +68,7 @@ function zeitfresser_toc_options( $wp_customize ) { 'section' => 'ztfr_general', 'label' => esc_html__( 'Show Article TOC', 'zeitfresser' ), 'description' => esc_html__( 'Enable floating TOC on single posts.', 'zeitfresser' ), - 'priority' => 21, + 'priority' => 11, ) ); @@ -67,9 +88,9 @@ function zeitfresser_toc_options( $wp_customize ) { array( 'type' => 'number', 'section' => 'ztfr_general', - 'label' => esc_html__( 'Minimum Headlines for TOC', 'zeitfresser' ), + 'label' => esc_html__( 'Minimum Headlines', 'zeitfresser' ), 'description' => esc_html__( 'TOC appears only if this number of headings is reached.', 'zeitfresser' ), - 'priority' => 22, + 'priority' => 12, 'input_attrs' => array( 'min' => 1, 'max' => 50, diff --git a/style.css b/style.css index 5457eab..bf1d150 100644 --- a/style.css +++ b/style.css @@ -5,7 +5,7 @@ Author: Zeitfresser Author URI: https://ztfr.eu/ Theme URI: https://ztfr.eu/ Description: Zeitfresser Wordpress Theme -Version: 2.2 +Version: 2.4 Tested up to: 6.2 Requires PHP: 7.0 License: GNU General Public License v2 or later