From 9bd7b80cf90e6e7a00a7dd1a06c3ea665c11d0e3 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Mon, 23 Feb 2026 15:52:57 +0100 Subject: [PATCH 1/2] Fix search tab activation regression: preserve active tab and pageshow timing Adapt openAllTabsetsContainingEl to: - Skip tab activation when the active pane already contains a match - Defer tab activation to pageshow so it runs after tabsets.js restores localStorage state (search activation wins over stored preference) - Scroll to first visible match after tab activation settles - Fix implicit global variable (const leaf) --- .../projects/website/search/quarto-search.js | 61 ++++++++++++++----- 1 file changed, 46 insertions(+), 15 deletions(-) diff --git a/src/resources/projects/website/search/quarto-search.js b/src/resources/projects/website/search/quarto-search.js index c67d7b1033..2fd4277f47 100644 --- a/src/resources/projects/website/search/quarto-search.js +++ b/src/resources/projects/website/search/quarto-search.js @@ -47,6 +47,19 @@ window.document.addEventListener("DOMContentLoaded", function (_event) { // perform any highlighting highlight(query, mainEl); + // Activate tabs on pageshow — after tabsets.js restores localStorage state. + // tabsets.js registers its pageshow handler during module execution (before + // DOMContentLoaded). By registering ours during DOMContentLoaded, listener + // ordering guarantees we run after tabsets.js — so search activation wins. + window.addEventListener("pageshow", function (event) { + if (!event.persisted) { + for (const mark of mainEl.querySelectorAll("mark")) { + openAllTabsetsContainingEl(mark); + } + requestAnimationFrame(() => scrollToFirstVisibleMatch(mainEl)); + } + }, { once: true }); + // fix up the URL to remove the q query param const replacementUrl = new URL(window.location); replacementUrl.searchParams.delete(kQueryArg); @@ -1170,7 +1183,7 @@ function searchMatches(inSearch, el) { /** @type {{i:number; els:Map}[]} */ let curMatchContext = initMatch() - for (leaf of leafNodes) { + for (const leaf of leafNodes) { const leafStr = leaf.textContent.toLowerCase() // for each character in this leaf's text: for (let leafi = 0; leafi < leafStr.length; leafi++) { @@ -1231,18 +1244,43 @@ function markMatches(node, lohis) { return parent } +// Activate ancestor tabs so a search match inside an inactive pane becomes visible. +// When multiple panes in the same tabset contain matches, avoid switching away from +// the currently active pane — the user already sees a match there. function openAllTabsetsContainingEl(el) { - for (const tab of matchAncestors(el, '.tab-pane')) { - const tabButton = document.querySelector(`[data-bs-target="#${tab.id}"]`); + for (const pane of matchAncestors(el, '.tab-pane')) { + const tabContent = pane.closest('.tab-content'); + if (!tabContent) continue; + const activePane = tabContent.querySelector(':scope > .tab-pane.active'); + if (activePane?.querySelector('mark')) continue; + const tabButton = document.querySelector(`[data-bs-target="#${pane.id}"]`); if (tabButton) new bootstrap.Tab(tabButton).show(); } } +function scrollToFirstVisibleMatch(mainEl) { + for (const mark of mainEl.querySelectorAll("mark")) { + let hidden = false; + let el = mark.parentElement; + while (el && el !== mainEl) { + if (el.classList.contains("tab-pane") && !el.classList.contains("active")) { + hidden = true; + break; + } + el = el.parentElement; + } + if (!hidden) { + mark.scrollIntoView({ behavior: "smooth", block: "center" }); + return; + } + } +} + /** - * e.g. + * e.g. * ```js * const m = new Map() - * + * * arrayMapPush(m, 'dog', 'Max') * console.log(m) // Map { dog->['Max'] } * @@ -1270,16 +1308,9 @@ function highlight(searchStr, el) { } } - const matchNodes = [...matchesGroupedByNode].map(([node, lohis]) => { - const matchNode = markMatches(node, lohis) - openAllTabsetsContainingEl(matchNode) - return matchNode - }) - // let things settle before scrolling - setTimeout(() => - matchNodes[0]?.scrollIntoView({ behavior: 'smooth', block: 'center' }), - 400 - ) + for (const [node, lohis] of matchesGroupedByNode) { + markMatches(node, lohis) + } } /* Link Handling */ From e220960bdfcc96f8fb0e75d808aa056f0cd840c0 Mon Sep 17 00:00:00 2001 From: Elliot Date: Tue, 24 Feb 2026 10:40:46 -0500 Subject: [PATCH 2/2] simplify scrollToFirstVisibleMatch --- .../projects/website/search/quarto-search.js | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/resources/projects/website/search/quarto-search.js b/src/resources/projects/website/search/quarto-search.js index 2fd4277f47..8deeda2f79 100644 --- a/src/resources/projects/website/search/quarto-search.js +++ b/src/resources/projects/website/search/quarto-search.js @@ -1260,16 +1260,10 @@ function openAllTabsetsContainingEl(el) { function scrollToFirstVisibleMatch(mainEl) { for (const mark of mainEl.querySelectorAll("mark")) { - let hidden = false; - let el = mark.parentElement; - while (el && el !== mainEl) { - if (el.classList.contains("tab-pane") && !el.classList.contains("active")) { - hidden = true; - break; - } - el = el.parentElement; - } - if (!hidden) { + const isMarkVisible = matchAncestors(mark, '.tab-pane').every(markTabPane => + markTabPane.classList.contains("active") + ) + if (isMarkVisible) { mark.scrollIntoView({ behavior: "smooth", block: "center" }); return; }