160d10a30597d7435c30409878e2ba11c3e7c660
mspeir
Wed Sep 9 14:19:40 2026 -0700
trackDb docs: stop the shared script from aborting on the pages without a search box, refs #38062
Two JavaScript faults on the trackDb doc pages, both of which trackDbHub.v3.html
had already avoided while the other pages drifted.
trackDbDoc.html, changes.html and trackDbHub.v0/v1/v2.html load /js/utils.js in
their head, and the hgMenubar include further down brings in the same file by a
different path, so it ran twice. utils.js declares mouseoverContainer with let
at top level, so the second run threw a redeclaration error. v3 does not load
utils.js in the head, and the menubar copy is in place well before anything
needs it, so drop the head copy from the other five.
documentLoad() then called addEventListener on the result of
getElementById("tdbSearch") with no null check, and only v3 has that input, so
the function threw partway through on the other four pages and never reached
its end. The search box and jump-to-top button do not exist on those pages, so
nothing visible was lost, but the exception aborted the rest of documentLoad
and would have swallowed anything added after it. Guard the two listeners.
All six pages now load with no console errors, checked in a browser.
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 c98ccf5cf2b..266e58215a4 100644
--- src/hg/htdocs/goldenPath/help/trackDb/trackDbDoc.js
+++ src/hg/htdocs/goldenPath/help/trackDb/trackDbDoc.js
@@ -1,1023 +1,1028 @@
// 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");
});
+ // Only the current hub spec page carries the search box; the other doc pages
+ // share this script and have no such input.
let inp = document.getElementById("tdbSearch");
+ if (inp) {
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(() => {
+ if (inp)
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';
}
}