blob: 39a05c4e4b31d4fd9879ab2ad90757a0f3d1342c [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/*
* Comet docs UX enhancements layered on top of pydata_sphinx_theme:
* - "Home" tab injected at the start of the top navbar (toctree can't
* self-reference, so the landing page link lives here)
* - Click-to-toggle expand/collapse arrows on sidebar nav groups
* - In-sidebar search filter with character highlighting (replaces the
* default Sphinx redirect-to-/search.html flow)
* - Verbose "Comet X.Y.Z User Guide" breadcrumb labels trimmed to "X.Y.Z"
* - Right-side TOC auto-hidden on pages with too few headings to navigate
*
* All behaviour is additive — pydata's existing keyboard navigation, theme
* switcher, search index, and toctree generation continue to work unchanged.
*/
(function () {
'use strict';
// ── Sidebar collapse/expand toggle buttons ─────────────────────────
// Pydata's toctree renders a flat <ul> tree with no toggles. We inject a
// real <button> for every nav item that has children so the user can
// expand/collapse sections independently of which page they're on.
function initSidebarToggle() {
// Match any toctree item with children, not just L1/L2 — Older Versions
// contains nested version groups (0.15.x, 0.14.x) at L3 that need their
// own expand/collapse arrows.
var items = document.querySelectorAll('.bd-links li[class*="toctree-l"]');
items.forEach(function (li) {
var ul = li.querySelector(':scope > ul');
var link = li.querySelector(':scope > a');
if (!ul || !link) return;
if (li.querySelector(':scope > .comet-toggle')) return;
// Auto-expand the section that contains the current page so the
// active page is always visible without an extra click.
if (li.classList.contains('current')) {
li.classList.add('comet-expanded');
}
var btn = document.createElement('button');
btn.type = 'button';
btn.className = 'comet-toggle';
btn.setAttribute('aria-label', 'Toggle section');
btn.setAttribute(
'aria-expanded',
li.classList.contains('comet-expanded') ? 'true' : 'false'
);
btn.addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
var expanded = li.classList.toggle('comet-expanded');
btn.setAttribute('aria-expanded', expanded ? 'true' : 'false');
});
link.parentNode.insertBefore(btn, link.nextSibling);
});
}
// ── In-sidebar search filter ────────────────────────────────────────
// Replaces Sphinx's "submit form → land on /search.html" flow with a
// live filter that hides non-matching nav items and highlights the
// matched characters in-place. Search of full document content still
// works via the regular Sphinx search page (pressing Enter is just
// suppressed here so the form doesn't navigate away).
function escapeRegex(s) {
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function escapeHtml(s) {
var map = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
return s.replace(/[&<>"']/g, function (c) { return map[c]; });
}
function initSidebarSearch() {
var input = document.querySelector(
'.bd-sidebar form.bd-search input[type="search"], ' +
'.bd-sidebar form.bd-search input.form-control'
);
var form = document.querySelector('.bd-sidebar form.bd-search');
var nav = document.querySelector('.bd-links');
if (!input || !nav) return;
if (form) {
form.addEventListener('submit', function (e) { e.preventDefault(); });
}
// Cache the original text so we can restore it (and re-highlight on
// every keystroke) without losing nav item labels.
var allLinks = Array.from(nav.querySelectorAll('a.reference'));
allLinks.forEach(function (a) {
if (!a.dataset.cometOrigText) {
a.dataset.cometOrigText = a.textContent;
}
});
var allListItems = Array.from(nav.querySelectorAll('li'));
function applyFilter(query) {
query = (query || '').trim();
if (!query) {
// Clear filter: restore original text, drop filter classes.
allLinks.forEach(function (a) {
a.innerHTML = escapeHtml(a.dataset.cometOrigText);
});
allListItems.forEach(function (li) {
li.classList.remove(
'comet-filter-hidden',
'comet-filter-match',
'comet-filter-expanded'
);
});
nav.classList.remove('comet-filtering');
return;
}
nav.classList.add('comet-filtering');
var re = new RegExp('(' + escapeRegex(query) + ')', 'ig');
allListItems.forEach(function (li) {
li.classList.add('comet-filter-hidden');
li.classList.remove('comet-filter-match', 'comet-filter-expanded');
});
allLinks.forEach(function (a) {
var orig = a.dataset.cometOrigText;
if (re.test(orig)) {
re.lastIndex = 0;
a.innerHTML = escapeHtml(orig).replace(
re,
'<mark class="comet-hl">$1</mark>'
);
// Reveal this item and every ancestor li so the match is reachable.
var li = a.closest('li');
while (li && nav.contains(li)) {
li.classList.remove('comet-filter-hidden');
li.classList.add('comet-filter-match', 'comet-filter-expanded');
li = li.parentElement.closest('li');
}
} else {
a.innerHTML = escapeHtml(orig);
}
});
}
var debounceTimer;
input.addEventListener('input', function () {
clearTimeout(debounceTimer);
var q = input.value;
debounceTimer = setTimeout(function () { applyFilter(q); }, 80);
});
input.addEventListener('keydown', function (e) {
if (e.key === 'Escape') {
input.value = '';
applyFilter('');
} else if (e.key === 'Enter') {
e.preventDefault();
}
});
}
// ── Inject "Home" link into the top navbar ─────────────────────────
// The toctree can't self-reference, so we add the Home tab via JS.
// It's prepended to the desktop nav AND the mobile drawer. The link
// is computed from the navbar logo's href so it stays correct at any
// page depth.
function initHomeNavLink() {
var logo = document.querySelector('a.navbar-brand');
var homeHref = logo ? logo.getAttribute('href') : null;
if (!homeHref) return;
var isHomePage = !!document.querySelector('.comet-hero');
document
.querySelectorAll('.bd-navbar-elements.navbar-nav')
.forEach(function (nav) {
if (nav.querySelector('.comet-home-nav-item')) return;
var li = document.createElement('li');
li.className = 'nav-item comet-home-nav-item';
if (isHomePage) li.classList.add('active', 'current');
var a = document.createElement('a');
a.className = 'nav-link nav-internal';
a.setAttribute('href', homeHref);
if (isHomePage) a.setAttribute('aria-current', 'page');
a.textContent = 'Home';
li.appendChild(a);
nav.insertBefore(li, nav.firstChild);
});
}
// ── Section titles toggle collapse/expand too ──────────────────────
// pydata's toctree hoists the link out of <details> for nav items that
// are both a page and a section header (e.g. "0.17.x (current)",
// "Expression Compatibility"), so by default only the chevron toggles;
// clicking the title just navigates. Since navigating to a page inside
// its own section makes that section "current" (auto-expanded), a title
// click reads as "expand-only" — clicking it again just reloads the same
// page, which is still expanded. Make the title toggle the same
// <details> the chevron does, so both controls behave symmetrically.
function initSectionTitleToggle() {
var titles = document.querySelectorAll('.bd-links li.has-children > a.reference');
titles.forEach(function (link) {
var details = link.nextElementSibling;
if (!details || details.tagName !== 'DETAILS') return;
link.setAttribute('role', 'button');
link.setAttribute('aria-expanded', details.open ? 'true' : 'false');
// Listen on <details>'s own toggle event, not just this link's click,
// so aria-expanded stays correct whether the chevron/summary or the
// title triggered the change.
details.addEventListener('toggle', function () {
link.setAttribute('aria-expanded', details.open ? 'true' : 'false');
});
link.addEventListener('click', function (e) {
e.preventDefault();
details.open = !details.open;
});
});
}
// ── Breadcrumb trim ────────────────────────────────────────────────
// "Comet 0.16.0-SNAPSHOT User Guide" reads as noise in a breadcrumb
// when the section is already shown in the top nav. Trim to "0.16.0-
// SNAPSHOT" and let the parent breadcrumb item carry the section name.
function initBreadcrumbTrim() {
document
.querySelectorAll('.bd-breadcrumbs .breadcrumb-item a')
.forEach(function (a) {
var match = a.textContent
.trim()
.match(/^Comet\s+([\d.x\-A-Za-z]+)\s+User Guide$/);
if (match) a.textContent = match[1];
});
}
// ── Right-side TOC visibility ──────────────────────────────────────
// Reference-style pages (config tables, expression lists) and short
// narrative pages don't benefit from an in-page TOC. Hide the rail
// when there are fewer than 3 anchors so it doesn't eat real estate
// for nothing.
function initSecondaryTocVisibility() {
var sidebar = document.querySelector('.bd-sidebar-secondary');
if (!sidebar) return;
var anchors = sidebar.querySelectorAll(
'nav.bd-toc a.reference, .page-toc a.reference'
);
if (anchors.length < 3) {
sidebar.classList.add('comet-toc-empty');
}
}
function bootstrap() {
initHomeNavLink();
initSidebarToggle();
initSectionTitleToggle();
initSidebarSearch();
initSecondaryTocVisibility();
initBreadcrumbTrim();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', bootstrap);
} else {
bootstrap();
}
})();