From aaae9ee388e3efeb44773467e8ba21745fb59e4f Mon Sep 17 00:00:00 2001 From: Frans de Jonge Date: Fri, 5 Jun 2026 15:31:42 +0200 Subject: [PATCH] Always jump article to top when header is offscreen, also when 'Stick the article to the top when opened' is disabled (#8870) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Always jump article to top when header is offscreen Closes #4069. How to test the feature manually: 1. Disable 'Stick the article to the top when opened' 2. Open a very long article 3. No scrolling will occur if you close it again while the header is still onscreen 4. If the header left the screen, opening another article will scroll to it to top 5. Same for collapsing the current article, but then it'll put the current header at the top * Compensate for layout shift * Fix going back to previous article above viewport * clarify variable name: header_off_screen → header_above_viewport --- p/scripts/main.js | 51 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 43 insertions(+), 8 deletions(-) diff --git a/p/scripts/main.js b/p/scripts/main.js index 0ac11d538..dda796766 100644 --- a/p/scripts/main.js +++ b/p/scripts/main.js @@ -463,6 +463,20 @@ function toggleContent(new_active, old_active, skipping) { loadLazyImages(new_active); } + const relative_move = context.current_view === 'global'; + const box_to_move = relative_move ? document.getElementById('panel') : document.scrollingElement; + + const old_scrollTop = box_to_move.scrollTop; + const old_offsetTop = new_active.offsetTop; + + const nav_menu = document.querySelector('.nav_menu'); + let nav_menu_height = 0; + if (nav_menu && (getComputedStyle(nav_menu).position === 'fixed' || getComputedStyle(nav_menu).position === 'sticky')) { + nav_menu_height = nav_menu.offsetHeight; + } + + const flux_header = new_active.querySelector('.flux_header'); + if (old_active !== new_active) { if (!skipping) { new_active.classList.add('active'); @@ -479,18 +493,22 @@ function toggleContent(new_active, old_active, skipping) { new_active.classList.toggle('active'); } - const relative_move = context.current_view === 'global'; - const box_to_move = relative_move ? document.getElementById('panel') : document.scrollingElement; + const new_offsetTop = new_active.offsetTop; + const layout_shift = new_offsetTop - old_offsetTop; - if (context.sticky_post) { // Stick the article to the top when opened - const prev_article = new_active.previousElementSibling; - const nav_menu = document.querySelector('.nav_menu'); - let nav_menu_height = 0; + const prev_article = new_active.previousElementSibling; - if (nav_menu && (getComputedStyle(nav_menu).position === 'fixed' || getComputedStyle(nav_menu).position === 'sticky')) { - nav_menu_height = nav_menu.offsetHeight; + let header_above_viewport = false; + + if (!context.sticky_post) { + // Compensate for layout shift to maintain visual position + box_to_move.scrollTop = old_scrollTop + layout_shift; + if (flux_header) { + header_above_viewport = flux_header.getBoundingClientRect().top < nav_menu_height; } + } + if (context.sticky_post || header_above_viewport) { // Stick the article to the top when opened, or when header is off-screen let new_pos = new_active.offsetParent.offsetTop + new_active.offsetTop - nav_menu_height; if (prev_article && prev_article.offsetParent && new_active.offsetTop - prev_article.offsetTop <= 150) { @@ -506,6 +524,23 @@ function toggleContent(new_active, old_active, skipping) { } box_to_move.scrollTop = new_pos; + } else { + // If the header is below the viewport, scroll down just enough to bring it fully into view + if (flux_header) { + let bottom = flux_header.getBoundingClientRect().bottom; + const inner_header = new_active.querySelector('.flux_content header'); + if (inner_header) { + bottom = Math.max(bottom, inner_header.getBoundingClientRect().bottom); + } + let overflow = bottom - window.innerHeight; + if (overflow > 0) { + const max_overflow = flux_header.getBoundingClientRect().top - nav_menu_height; + if (overflow > max_overflow) { + overflow = max_overflow > 0 ? max_overflow : 0; + } + box_to_move.scrollTop += overflow; + } + } } if (new_active.classList.contains('active') && !skipping) {