bc030946a04b1f5c4c0a1501e0eb385747238b1b mspeir Wed Sep 9 11:43:51 2026 -0700 trackDb docs: send a library cross-link to the current spec when the host page has no such anchor, refs #38062 trackDbLibrary.shtml is shared by trackDbDoc.html and all four trackDbHub version pages. Its setting blurbs cross-reference other settings with a bare fragment, but the anchor is written by each host page's own table of contents, so a blurb that mentions a setting newer than a frozen hub spec has no target on that page. trackDbHub.v0, v1 and v2 therefore carry dangling links to filterLabel, detailsDynamicTable, filterBy, faceted_composite, onlyVisibility and detailsStaticTable, and the library is regenerated while those snapshots stay put, so the set grows with every new setting. Rather than edit the frozen snapshots or hard-code a page into the library, check at the moment a blurb is moved out of the hidden library into the document and repoint the link to trackDbHub.html, which always serves the current spec, when the anchor is not on the page. Links whose target is present are untouched, so this changes nothing on trackDbDoc.html or on the current spec, and it keeps working as settings are added. Only the visibility blurb's link to faceted_composite is reachable by a reader today; the rest sit in blurbs those pages do not display. Verified in a browser on all five pages. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> diff --git src/hg/htdocs/goldenPath/help/trackDb/trackDbDoc.js src/hg/htdocs/goldenPath/help/trackDb/trackDbDoc.js index 1f2bcb5902c..c98ccf5cf2b 100644 --- src/hg/htdocs/goldenPath/help/trackDb/trackDbDoc.js +++ src/hg/htdocs/goldenPath/help/trackDb/trackDbDoc.js @@ -1,1004 +1,1023 @@ // Used by trackDbDoc.html and friends function jumpTo(obj) { // jumps to an anchor based upon text of obj //alert("url:"+window.location+"#"+obj.text); obj.href="#"+obj.text; return true; } var tdbDoc = { sortNoCase: function (x,y) { // case insensitive sort routine // perhaps move this to utils.js // NOTE: localeCompare() is not supported by all browsers. var a = String(x).toUpperCase(); var b = String(y).toUpperCase(); if (a > b) return 1 if (a < b) return -1 return 0; }, library: { // separate namespace for library handling checkClasses: function (classes) { // trackDbTestBlurb.html only // Walks through the classes and ensures they are unique //warn("class count:"+classes.length); $(classes).each(function (ix) { var divs = $('div#library').find( 'div.'+this ); if (divs.length > 1) { warn("Found more than one DIV of class='"+this+"' in library."); return false; } }); return true; }, checkBlurb: function (div) { // trackDbTestBlurb.html only // check that a single div is conforming and return classes var classes = $(div).attr("class").split(" "); if (classes.length > 1) { warn('DIV with more than one class: '+ classes); } else if (classes.length === 1) { if (classes[0].indexOf('_intro') === -1) { var types = $(div).find('span.types'); if (types.length === 0) warn('DIV '+classes[0]+' missing "types" span.'); else if (types.length > 1) warn('DIV '+classes[0]+' more than one "types" span:'+types.length); var format = $(div).find('div.format'); if (format.length === 0) warn('DIV '+classes[0]+' missing "format" DIV.'); else if (format.length > 1) warn('DIV '+classes[0]+' more than one "format" DIV:'+format.length); } } return classes; }, checkup: function () { // trackDbTestBlurb.html only // This routine runs the library through unique class check var allClasses = []; $('div#library').find('div').not('.format,.hintBox').each(function (ix) { var classes = tdbDoc.library.checkBlurb(this); if ( classes.length === 0 ) return true; allClasses.push(classes); }); return tdbDoc.library.checkClasses(allClasses); }, lookup: function (id,verbose) { // Standard lookup for library blurbs var blurb = $('div#library').find('div.'+id); if (blurb.length > 1 && verbose) { warn("Non unique ID found in library: "+ id); } else if (blurb.length === 0) { // There can be several references to the same details blurb in // the document. So not finding it may mean it has already been // moved from the library. Find an clone that one. blurb = $("div."+id).first(); if (blurb.length === 0 && verbose) warn("Missing document blurb for ID: "+ id); else { var format = $(blurb).parent('td').find('.format'); blurb = $(blurb).clone(); $(blurb).attr('id',''); // cloned blurb cannot have unique ID if (format.length === 1) { format = $(format).clone(); $(blurb).append(format); } } } // Fill in tag 'level' if any from calling HTML doc var spec = $('div#specification').find('td.'+id); if (spec.length === 0) { return blurb; } var code = $(spec).find('code'); if (code.length === 0) { return blurb; } var level = $(code).attr('class'); if (level && level.length !== 0) { var start = $(blurb).find('p').first(); if ($(start).attr('class') !== 'level') { $(start).before('<p class="level">Support level: ' + '<span class=' + level + '>' + level.replace('level-','') + '</span></p>'); } $(blurb).find('code').addClass(level); } return blurb; } }, toc: { // separate namespace for self-assembling table of contents blurbCells: function () { // returns list of cells that are to be filled with blurbs return $('table.settingsTable').find('td:has(div.format)'); }, blurblessCells: function () { // returns list of setting cells that are not in within library var tbls = $('table.settingsTable').not('#alphabetical,#toc,:has(div.format)'); if (tbls.length === 0) return []; return $(tbls).find("td:has(A)"); }, classesFromObjects: function (objs,onlyOne) { // converts objs array to classes array. Warns if obj does not have exactly 1 class var allClasses = []; $(objs).each(function (ix) { var classesTemp = this.getAttribute("class"); var classes = classesTemp !== null ? classesTemp.split(" ") : [""]; if (classes.length > 1 && onlyOne) warn('obj with more than one class: '+ classes); else if (classes.length === 0 && onlyOne) warn('obj ' + this.name + ' with no classes.'); else allClasses.push( classes ); // classes string array is [ [ str1], [str2] ] }); return allClasses; }, namesFromContainedAnchors: function (cells) { // Special case code for getting hidden settings that are only documented within // another settings blob (example viewLimitsMax) // returns list of named anchors within cells which should detect these hidden settings var allNames = []; $(cells).each(function (ix) { var cell = this; var anchors = $(cell).children("A[name!='']"); if (anchors.length > 1) { // one is expected and not interesting var aClass = String( $(cell).attr('class') ); // NEVER name var 'class' !!!!! $(anchors).each(function (ix) { var name = String( $(this).attr('name') ); if (name !== aClass) allNames.push( [ name ] ); // string array best as [ [ str1], [str2] ] }); } }); return allNames; }, classesExcludeSuffixes: function (classes, suffixes) { // Since class list will include "_example" classes... // returns classes after those with unwanted suffixes are removed var cleanClasses = []; while (classes.length > 0) { // unless string array is [ [ str1], [str2] ] var aClass = classes.shift(); // shift/pop gets lost! for(var ix=0;ix<suffixes.length;ix++) { if (aClass[0].indexOf( suffixes[ix] ) !== -1) break; } if (ix === suffixes.length) cleanClasses.push(aClass); } return cleanClasses; }, makePlainRow: function (aClass) { var level = 'level-new'; var div = tdbDoc.library.lookup(aClass,false); if (div.length === 1) { level = $(div).find('code').attr('class'); if (!level) { level = 'level-new'; } } return aClass + '\t' + level.replace('level-','') + '\n'; }, makeRow: function (aClass) { // Puts together a single row into a self assembling table of contents // Find types var types = ""; var div = tdbDoc.library.lookup(aClass, false); var level = null; if (div.length === 1) { level = $(div).find('code').attr('class'); if (!level) { level = null; } var spanner = $(div).find('span.types'); if (spanner.length === 1) { var classes = $(spanner).attr("class").split(" "); classes = aryRemove(classes,['types']); $(classes).each(function (ix) { types += " <A onclick='return jumpTo(this);' HREF='#'>" + this + "</a>"; }); } } // Where documented (what table)? // TODO: Should rewrite classes array to also carry the table. var best = ''; var td = $('td.' + aClass); var tbl; if (td.length > 0) // Always chooses first tbl = $(td[0]).parents('table.settingsTable'); else tbl = $('table.settingsTable').has("a[name='"+aClass+"']"); if (tbl != undefined && tbl.length === 1) { var id = $(tbl[0]).attr('id'); if (id.length > 0) { best = "<A HREF='#" + id + "'>" +id.replace(/_/g," ") + "</a>" //if (td.length > 1) // best += " found "+td.length; } } var row = "<tr><td><A onclick='return jumpTo(this);' HREF='#'>" + aClass + "</a></td>"; if (tdbDoc.isHubDoc()) { // hide superfluous 'Required' paragraphs $('p.isRequired').hide(); //include level in the table if (level === null) { // settings w/o a 'blurb' can still have a level (e.g. deprecated settings) level = $(td[0]).find('code').attr('class'); if (typeof level === 'undefined') { var span = $('span.' + aClass); level = $(span[0]).find('code').attr('class'); if (typeof level === 'undefined') { level = 'level-'; } } } row += "<td class=" + level + ">" +level.replace('level-','') + "</span></td>"; } row += "<td>" + types + "</td><td>" + best + "</td></tr>"; return row; }, excludeClasses: [], assemble: function () { // assembles (or extends) a table of contents if on is found (Launched by timer) var tocTable = $('table#toc'); var plainTable = $('#plainToc'); if (tocTable.length === 1) { var cells = tdbDoc.toc.blurbCells(); // Most documented settings are found here var names = tdbDoc.toc.namesFromContainedAnchors(cells);// settings in others' cells cells = $(cells).add( tdbDoc.toc.blurblessCells() ); // undocumented: losers var classes = tdbDoc.toc.classesFromObjects(cells,true);// classes are setting names classes = classes.concat( names ); // hidden names wanted to if (classes.length === 0) return; // clean up and sort classes = tdbDoc.toc.classesExcludeSuffixes(classes,['_intro','_example']); if (classes.length === 0) return; if (tdbDoc.toc.excludeClasses.length > 0) { aryRemove( classes, tdbDoc.toc.excludeClasses ); tdbDoc.toc.excludeClasses = []; } classes.sort( tdbDoc.sortNoCase ); // Should now have a full set of settings to add to TOC var cols = tdbDoc.isHubDoc() ? 4 : 3; if ($(tocTable).find('thead').length === 0) { $(tocTable).prepend( "<THEAD><TR><TD colspan='" + cols + "'>"+ "<H3>Table of Contents</H3></TD></TR></THEAD>" ); } $(tocTable).append("<tbody>"); if ($(tocTable).find('th').length === 0) { var th = "<TR VALIGN=TOP><TH WIDTH=100>Setting</TH>"; if (tdbDoc.isHubDoc()) { th += "<TH>Level</TH>"; } th += "<TH WIDTH='25%'>For Types</TH><TH>Documented</TH></TR>"; $(tocTable).append(th); } var lastClass = ''; $(classes).each(function (ix) { if (lastClass !== String(this)) {// skip duplicates lastClass = String(this); $(tocTable).append(tdbDoc.toc.makeRow(this)); $(plainTable).append(tdbDoc.toc.makePlainRow(this)); } }); $(tocTable).append("</tbody>"); // TODO: Nice to do: allow for seeding toc with user defined rows. // Currently they would be before the rows assembled, but we would want to sort! } } }, tocAssemble: function (timeout,excludeClasses) { // Launches tocAssembly on timer if (excludeClasses != undefined && excludeClasses.length > 0) tdbDoc.toc.excludeClasses = excludeClasses; else tdbDoc.toc.excludeClasses = []; if (timeout > 0) setTimeout(tdbDoc.toc.assemble, timeout); else tdbDoc.toc.assemble(); }, polishButton: function (obj,openUp) { // Called to polish the toggle buttons after an action if (openUp) { obj.src='/images/remove_sm.gif'; obj.alt='-'; obj.title='hide details'; } else { obj.src='/images/add_sm.gif'; obj.alt='+'; obj.title='show details'; } }, + repointStaleLinks: function (scope) { + // A library blurb can cross-reference a setting that the host page does not list. + // The anchor for a setting is written by that page's own table of contents, so on a + // frozen older hub spec (trackDbHub.v0/v1/v2.html) such a link has no target here. + // Point those at trackDbHub.html, which always serves the current spec and does + // document the setting. Links whose target is on the page are left alone, so this + // is a no-op on trackDbDoc.html and on the current spec. + $(scope).find("a[href^='#']").each(function () { + var name = $(this).attr('href').substring(1); + if (name.length === 0) + return; + if (document.getElementById(name) === null + && document.getElementsByName(name).length === 0) + $(this).attr('href','trackDbHub.html#' + name); + }); + }, + loadIntro: function (div) { // Called at startup to load each track type intro from the library var id = $(div).attr('id'); var intro = tdbDoc.library.lookup(id,true); if (intro.length === 1) { $(intro).addClass("intro"); $(intro).detach(); $(div).replaceWith(intro); + tdbDoc.repointStaleLinks(intro); } }, findDetails: function (td) { // Gets the setting description out of the library and puts it in place var id = $(td).attr('class'); var details = tdbDoc.library.lookup(id,true); if (details.length === 1) { //var detail = $(details).clone(); var detail = $(details).detach(); //$(detail).attr('id',''); // keep unique ID $(detail).addClass("details"); var format = $(detail).find("div.format"); if (format.length === 1) { $(format).detach(); $(td).find("div.format").replaceWith(format); } //$(details).clone().appendTo(td); //$(details).detatch(); $(td).append(detail); + tdbDoc.repointStaleLinks(detail); } return $(td).find('div.details'); }, toggleDetails: function (obj,openUp) { // Called to toggle open or closed a single detail div or a related set var td = $(obj).parents('td')[0]; var details = $(td).find('div.details'); if (details == undefined || details.length !== 1) { details = tdbDoc.findDetails(td); } if (details == undefined || details.length > 1) { warn("Can't find details for id:" + td.id); return false; } details = details[0]; // determine recursion var tr = $(obj).parents('tr')[0]; var set = $(tr).hasClass('related') || $(tr).hasClass('related1st'); if (openUp == undefined) openUp = ( obj.alt === '+' ); else set = false; // called for set only if openOp is undefined // Handle recursion if (set) { var setClass = $( obj ).attr("class").replace(/ /g,"."); var buttons = $(tr).parents('table').find("img." + setClass); $(buttons).each(function (ix) { tdbDoc.toggleDetails(this,openUp); }); return true; } // Just one if (openUp) $(details).show(); else $(details).hide(); tdbDoc.polishButton(obj,openUp); return true; }, toggleTable: function (obj,openUp) { // Called to toggle open or closed details in a table if (openUp == undefined) openUp = ( obj.alt === '+' ); var buttons = $(obj).parents('table').find('img.toggle.detail'); $(buttons).each(function (ix) { tdbDoc.toggleDetails(this,openUp); }); tdbDoc.polishButton(obj,openUp); return true; }, _toggleAll: function (obj,openUp) { // Called to toggle open or closed details in a table if (openUp == undefined) openUp = ( obj.alt === '+' ); var buttons = $('img.toggle.oneSection'); $(buttons).each(function (ix) { tdbDoc.toggleTable(this,openUp); }); tdbDoc.polishButton(obj,openUp); return true; }, toggleAll: function (obj,openUp) { waitOnFunction( tdbDoc._toggleAll, obj, openUp); }, isHubDoc: function () { return $('#trackDbHub_version').length !== 0; }, documentLoad: function () { // Called at $(document).ready() to load a trackDb document page var divIntros = $("div.intro").each( function (ix) { tdbDoc.loadIntro(this); }); $('div.details').hide(); $('img.toggle').each(function (ix) { var obj = this; tdbDoc.polishButton(obj,false); if ($(obj).hasClass('all')) { $(obj).click(function () { return tdbDoc.toggleAll(obj); }); } else if ($(obj).hasClass('oneSection')) { $(obj).click(function () { return tdbDoc.toggleTable(obj); }); } else { $(obj).click(function () { return tdbDoc.toggleDetails(obj); }); } }); if (document.URL.search('#allOpen') !== -1) tdbDoc.toggleAll($('img.toggle.all')[0],true); document.addEventListener("keydown", e => { if (e.key === "F3") { e.preventDefault(); jumpToResult(e.shiftKey ? -1 : 1); } }); // add the tdb setting to the img toggle classList let toggles = document.querySelectorAll("IMG.toggle.detail"); toggles.forEach( (toggle) => { toggle.classList.add(toggle.parentNode.className + "_imgToggle"); }); let inp = document.getElementById("tdbSearch"); inp.addEventListener("input", (e) => { let term = inp.value.trim(); if (term.length >= 2) { runSearch(term); } else if (term.length === 1) { // Show helpful message for single character runSearch(term); } else if (term.length === 0) { // Clear search when input is empty clearSearchHighlights(); hideSearchStatus(); currentResults = []; currentIndex = -1; } }); // Add keyboard shortcuts for search navigation inp.addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); if (currentResults.length > 0) { jumpToResult(e.shiftKey ? -1 : 1); } } else if (e.key === "Escape") { e.preventDefault(); clearSearch(); } }); // Add clear search button functionality let clearButton = document.getElementById("clearSearch"); if (clearButton) { clearButton.addEventListener("click", () => { clearSearch(); }); } // Add jump to top functionality with smooth scrolling let jumpToTopButton = document.getElementById("jumpToTop"); if (jumpToTopButton) { jumpToTopButton.addEventListener("click", (e) => { e.preventDefault(); // Smooth scroll to top window.scrollTo({ top: 0, behavior: "smooth" }); // Also focus the search input for easy continued searching setTimeout(() => { inp.focus(); }, 500); }); } } } function searchHidden(term) { // Reset previous search clearSearchHighlights(); // track elements already processed to prevent repeated <mark>ing if (!window.__searchedElements) { window.__searchedElements = new WeakSet(); } if (!term || term.length < 2) { return []; } let results = []; let escapedTerm = escapeRegexSpecialChars(term); // Strategy: Search ALL content and sort results by document position // First, collect all searchable elements let allElements = []; // 1. Add visible format divs (which contain code blocks with setting names) let formatDivs = document.querySelectorAll('div.format'); formatDivs.forEach(element => { // Skip if this element is inside the library (which is hidden) or already in details if (!element.closest('#library') && !element.closest('div.details')) { allElements.push(element); } }); // 2. Add standalone code and pre elements (not inside format divs or details) let codeElements = document.querySelectorAll('code, pre'); codeElements.forEach(element => { // Skip if inside library, details, or format divs (already searched above) if (!element.closest('#library') && !element.closest('div.details') && !element.closest('div.format')) { allElements.push(element); } }); // 3. Add other table content that might contain searchable text let tableCells = document.querySelectorAll('table.settingsTable td'); tableCells.forEach(element => { // Skip if inside library or if it contains details/format (avoid duplication) if (!element.closest('#library') && !element.querySelector('div.details') && !element.querySelector('div.format')) { allElements.push(element); } }); // 4. Add all already-opened details sections let existingDetails = document.querySelectorAll('div.details'); existingDetails.forEach(div => { allElements.push(div); }); // Sort elements by their position in the document (top to bottom) allElements.sort((a, b) => { let position = a.compareDocumentPosition(b); if (position & Node.DOCUMENT_POSITION_FOLLOWING) { return -1; // a comes before b } else if (position & Node.DOCUMENT_POSITION_PRECEDING) { return 1; // a comes after b } return 0; // same position }); // Now search through elements in document order allElements.forEach(element => { // skip elements already searched if (window.__searchedElements.has(element)) { return; } window.__searchedElements.add(element); // skip anything inside an existing <mark> if (element.closest && element.closest("mark")) { return; } let foundMatches = searchAndHighlightInElement(element, escapedTerm); if (foundMatches.length > 0) { results.push(...foundMatches); } }); // 5. Find all toggle elements that might have hidden content not yet opened let toggles = [...document.querySelectorAll("[class]")].filter(el => [...el.classList].some(c => c.endsWith("_imgToggle"))); // Sort toggles by document position too toggles.sort((a, b) => { let position = a.compareDocumentPosition(b); if (position & Node.DOCUMENT_POSITION_FOLLOWING) { return -1; } else if (position & Node.DOCUMENT_POSITION_PRECEDING) { return 1; } return 0; }); toggles.forEach((toggle) => { let detailsId = toggle.parentNode.className; if (!detailsId) return; let td = toggle.parentNode; let existingDetails = $(td).find('div.details'); // Only search library content if details are not already visible if (existingDetails.length === 0) { if (window.__searchedElements.has(td)) { return; } window.__searchedElements.add(td); let blurb = tdbDoc.library.lookup(detailsId, false); if (blurb && blurb.length > 0) { // Check if library content contains the search term let hasMatch = false; blurb.each((ix, div) => { if (div.textContent && div.textContent.toLowerCase().includes(term.toLowerCase())) { hasMatch = true; return false; // break out of jQuery each } }); if (hasMatch) { // Show the details to move content from library to document tdbDoc.toggleDetails(toggle, true); // Now search the newly visible content and insert results in correct position let newDetails = $(td).find('div.details'); let newMatches = []; newDetails.each((ix, docDiv) => { // avoid re-searching if run multiple times if (window.__searchedElements.has(docDiv)) { return; } window.__searchedElements.add(docDiv); // skip elements inside <mark> if (docDiv.closest && docDiv.closest("mark")) { return; } let docMatches = searchAndHighlightInElement(docDiv, escapedTerm); newMatches.push(...docMatches); }); // Insert new matches in the correct position relative to existing results if (newMatches.length > 0) { // Find where to insert these results to maintain document order let insertIndex = results.length; for (let i = 0; i < results.length; i++) { let position = newMatches[0].compareDocumentPosition(results[i]); if (position & Node.DOCUMENT_POSITION_FOLLOWING) { insertIndex = i; break; } } results.splice(insertIndex, 0, ...newMatches); } } } } }); return results; } function escapeRegexSpecialChars(str) { // Escape special regex characters to treat search term as literal text return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } function clearSearchHighlights() { // More robust cleanup of previous search highlights document.querySelectorAll("mark").forEach(mark => { let parent = mark.parentNode; if (parent) { // Replace mark with its text content let textNode = document.createTextNode(mark.textContent); parent.replaceChild(textNode, mark); // Normalize to merge adjacent text nodes parent.normalize(); } }); } function clearHighlightsInElement(element) { // Clear highlights only within a specific element if (!element) return; let marks = element.querySelectorAll("mark"); marks.forEach(mark => { let parent = mark.parentNode; if (parent) { let textNode = document.createTextNode(mark.textContent); parent.replaceChild(textNode, mark); parent.normalize(); } }); } function searchAndHighlightInElement(element, escapedTerm) { let results = []; let regexFactory = () => new RegExp(`(${escapedTerm})`, "gi"); // track processed text nodes so they don't get re-<mark>ed if (!window.__processedTextNodes) { window.__processedTextNodes = new WeakSet(); } // Create a tree walker to traverse only text nodes let walker = document.createTreeWalker( element, NodeFilter.SHOW_TEXT, null, false ); let textNodes = []; let node; // Collect all text nodes first to avoid modifying tree during traversal while (node = walker.nextNode()) { if (node.textContent.trim().length > 0) { // skip already processed nodes if (window.__processedTextNodes.has(node)) continue; // skip anything inside a <mark> if (node.parentNode.closest && node.parentNode.closest("mark")) continue; window.__processedTextNodes.add(node); textNodes.push(node); } } // Process each text node textNodes.forEach(textNode => { let text = textNode.textContent; // create a fresh regex to avoid lastIndex corruption const regex = regexFactory(); if (regex.exec(text)) { // Create a document fragment to hold the new nodes let fragment = document.createDocumentFragment(); let lastIndex = 0; let match; // Reset regex lastIndex for global search regex.lastIndex = 0; while ((match = regex.exec(text)) !== null) { // Add text before match if (match.index > lastIndex) { fragment.appendChild( document.createTextNode(text.substring(lastIndex, match.index)) ); } // Add highlighted match let mark = document.createElement('mark'); mark.textContent = match[1]; fragment.appendChild(mark); results.push(mark); lastIndex = regex.lastIndex; // Prevent infinite loop on zero-length matches if (match.index === regex.lastIndex) { regex.lastIndex++; } } // Add remaining text after last match if (lastIndex < text.length) { fragment.appendChild( document.createTextNode(text.substring(lastIndex)) ); } // Replace the original text node with the fragment textNode.parentNode.replaceChild(fragment, textNode); } }); return results; } let currentIndex = -1; let currentResults = []; function jumpToResult(direction) { if (currentResults.length === 0) { return; } currentIndex += direction; if (currentIndex >= currentResults.length) { currentIndex = 0; } if (currentIndex < 0) { currentIndex = currentResults.length - 1; } // Clear previous active results currentResults.forEach( (r) => { if (r && r.classList) { r.classList.remove("active-result"); } }); let target = currentResults[currentIndex]; if (target && target.classList) { target.classList.add("active-result"); // Ensure the element is visible and scroll to it scrollToResult(target); } else { console.log("Target result element not found or invalid"); } } function scrollToResult(target) { if (!target) return; // Function to attempt scrolling with retries function attemptScroll(retries = 3) { if (retries <= 0) { console.log("Failed to scroll to result after retries"); return; } // Check if the target is visible in the DOM if (target.offsetParent !== null) { console.log("Scrolling to result"); target.scrollIntoView({behavior: "smooth", block: "center"}); } else { // Target might be hidden, try to find its visible container let container = target.closest('div.details'); if (container && container.offsetParent !== null) { console.log("Scrolling to result container"); container.scrollIntoView({behavior: "smooth", block: "start"}); } else { // Wait a bit and try again console.log(`Retrying scroll (${retries} retries left)`); setTimeout(() => attemptScroll(retries - 1), 50); } } } attemptScroll(); } function runSearch(term) { // Clear previous results currentResults = []; currentIndex = -1; // Reset processed element tracking on each search window.__searchedElements = new WeakSet(); window.__processedTextNodes = new WeakSet(); if (!term || term.length < 2) { // Show message for short terms if (term.length === 1) { showSearchStatus(term, 0); } else { hideSearchStatus(); } return; } currentResults = searchHidden(term); // Always show search results count for valid searches showSearchStatus(term, currentResults.length); if (currentResults.length > 0) { // Small delay to ensure any new details sections are fully opened and rendered setTimeout(() => { jumpToResult(1); }, 150); } } function clearSearch() { let inp = document.getElementById("tdbSearch"); if (inp) { inp.value = ""; } clearSearchHighlights(); hideSearchStatus(); currentResults = []; currentIndex = -1; if (inp) { inp.blur(); } } function showSearchStatus(term, count) { let statusDiv = document.getElementById('searchStatus'); if (!statusDiv) { statusDiv = document.createElement('div'); statusDiv.id = 'searchStatus'; statusDiv.style.cssText = ` margin: 5px 0; padding: 5px 10px; background-color: #e8f4f8; border: 1px solid #bee5eb; border-radius: 3px; font-size: 12px; color: #0c5460; display: block; `; let searchDiv = document.getElementById('searchDiv'); if (searchDiv) { searchDiv.appendChild(statusDiv); } else { console.log('ERROR: Could not find searchDiv to append status'); } } // Ensure the status div is visible statusDiv.style.display = 'block'; if (count === 0) { if (term.length === 1) { statusDiv.textContent = `Type at least 2 characters to search`; statusDiv.style.backgroundColor = '#fff3cd'; statusDiv.style.borderColor = '#ffeaa7'; statusDiv.style.color = '#856404'; } else { statusDiv.textContent = `No results found for "${term}"`; statusDiv.style.backgroundColor = '#f8d7da'; statusDiv.style.borderColor = '#f5c6cb'; statusDiv.style.color = '#721c24'; } } else { statusDiv.textContent = `Found ${count} result${count !== 1 ? 's' : ''} for "${term}" - Use F3/Enter (Shift+F3/Shift+Enter) to navigate`; statusDiv.style.backgroundColor = '#e8f4f8'; statusDiv.style.borderColor = '#bee5eb'; statusDiv.style.color = '#0c5460'; } } function hideSearchStatus() { let statusDiv = document.getElementById('searchStatus'); if (statusDiv) { statusDiv.style.display = 'none'; } }