Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 76 additions & 14 deletions code-group-language-persist.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,18 @@
* LANGUAGES — only Python/TypeScript). The whitelist stops both descriptive
* tabs ("Web App", "Summary metrics") and incidental code tabs ("Bash",
* "cURL") from clobbering the saved language.
* - Restore: on load, after hydration, and on SPA route changes (a
* MutationObserver), any tab whose label matches the saved language (compared
* case-insensitively) is selected. Tabs already active are skipped, so this
* coexists with Mintlify's own <CodeGroup> restore without fighting it.
* - Restore: on load, after hydration, and on SPA route changes (detected by
* a MutationObserver watching location.pathname), any tab whose label
* matches the saved language (compared case-insensitively) is selected.
* Tabs already active are skipped, so this coexists with Mintlify's own
* <CodeGroup> restore without fighting it.
* - Yield: when the reader manually picks a non-language tab ("HTTP API",
* "Command Line", …), its group is marked overridden and restore leaves it
* alone for the rest of that page view. Restore also only re-runs when the
* pathname actually changes, never on arbitrary DOM mutations — otherwise
* the mutation caused by the reader's own tab switch would re-trigger a
* restore that instantly snaps mixed groups (e.g. Python / TypeScript /
* HTTP API) back to the saved language, making other tabs unselectable.
*
* Selection uses a real pointer sequence, not element.click(): Mintlify's tabs
* activate on pointer/mouse-down, and a synthetic click() does not switch them.
Expand Down Expand Up @@ -60,6 +68,14 @@
python: 1, typescript: 1
};

/**
* Tab groups the reader has manually steered to a non-language tab, which
* restore() must then leave alone. Keyed by the group's DOM node, so entries
* evaporate naturally when a client-side route change replaces the page
* content (and with it, the nodes).
*/
var overridden = typeof WeakSet === 'function' ? new WeakSet() : null;

/** Read the saved language, tolerant of disabled/throwing localStorage. */
function read() {
try { return localStorage.getItem(STORAGE_KEY); } catch (e) { return null; }
Expand Down Expand Up @@ -99,6 +115,16 @@
tab.getAttribute('aria-selected') === 'true';
}

/**
* The tab group a tab belongs to — its ARIA tablist, or failing that its
* parent element. Used to scope manual-override tracking per group.
* @param {Element} tab
* @returns {Element|null}
*/
function groupOf(tab) {
return tab.closest('[role="tablist"]') || tab.parentElement;
}

/**
* Selects a tab the way a real click does. Mintlify's tabs activate on
* pointer/mouse-down, not on a synthetic element.click(), so dispatch a full
Expand All @@ -116,19 +142,32 @@

/**
* Capture-phase selection handler. Records the chosen tab's label when it
* names a language, so the choice survives navigation. Bound to pointerdown
* (when tabs actually select) and click (covers keyboard activation).
* names a language, so the choice survives navigation; when it names
* anything else ("HTTP API", "Command Line", …), marks the group as
* reader-overridden so restore() stops re-asserting the saved language in
* it. Bound to pointerdown (when tabs actually select) and click (covers
* keyboard activation). Only trusted events count — the synthetic clicks
* restore() dispatches must not register as reader choices.
* @param {Event} e
*/
function onSelect(e) {
if (!e.isTrusted) return;
var tab = e.target && e.target.closest ? e.target.closest('[role="tab"]') : null;
if (!tab) return;
var label = labelOf(tab);
if (label && isLanguage(label)) write(label);
if (!label) return;
var group = overridden && groupOf(tab);
if (isLanguage(label)) {
write(label);
if (group) overridden.delete(group);
} else if (group) {
overridden.add(group);
}
}

/**
* Selects the saved language in every group that isn't already on it.
* Selects the saved language in every group that isn't already on it,
* except groups the reader has manually overridden this page view.
* Self-terminating: tabs already active are skipped, so the synthetic clicks
* we trigger here do not cause an observe/click feedback loop, and we never
* fight Mintlify's own already-applied <CodeGroup> selection.
Expand All @@ -140,9 +179,12 @@
var tabs = document.querySelectorAll('[role="tab"]');
for (var i = 0; i < tabs.length; i++) {
var tab = tabs[i];
if (labelOf(tab).toLowerCase() === want && !isActive(tab)) {
activate(tab);
if (labelOf(tab).toLowerCase() !== want || isActive(tab)) continue;
if (overridden) {
var group = groupOf(tab);
if (group && overridden.has(group)) continue;
}
activate(tab);
}
}

Expand All @@ -154,18 +196,38 @@
setTimeout(function () { pending = false; restore(); }, 80);
}

/** Restore now-ish, then again while late-hydrating content settles. */
function restoreSoon() {
schedule();
[300, 800, 1500].forEach(function (ms) { setTimeout(restore, ms); });
}

/**
* Mutation handler: re-applies the saved language only when the mutation
* burst accompanies an actual client-side navigation (the pathname
* changed). Restoring on every mutation would treat the reader's own tab
* switch — itself a DOM mutation — as a cue to snap the group back to the
* saved language.
*/
var lastPathname = location.pathname;
function onMutate() {
if (location.pathname === lastPathname) return;
lastPathname = location.pathname;
restoreSoon();
}

function start() {
// Capture phase so we observe the selection around Mintlify's own handling.
document.addEventListener('pointerdown', onSelect, true);
document.addEventListener('click', onSelect, true);

// Initial load: restore immediately, then catch tabs that hydrate later.
restore();
// Catch tabs that hydrate after first paint.
[300, 800, 1500].forEach(function (ms) { setTimeout(restore, ms); });
restoreSoon();

// Re-apply whenever new content renders (client-side route changes).
// Re-apply on client-side route changes.
if (document.body) {
new MutationObserver(schedule).observe(document.body, {
new MutationObserver(onMutate).observe(document.body, {
childList: true,
subtree: true
});
Expand Down