ca790256fe78136e840f27bd251e932fd0658e0c
chmalee
Thu Dec 11 12:39:30 2025 -0800
Fix bug in trackDbHub doc search: we were re-doing the search after marking up the first set of found elements, causing multiple highlights for a single match, which made cycling through found elements behave oddly, refs #36831
diff --git src/hg/htdocs/goldenPath/help/trackDb/trackDbDoc.js src/hg/htdocs/goldenPath/help/trackDb/trackDbDoc.js
index 2ff43e9da2c..08083e94484 100644
--- src/hg/htdocs/goldenPath/help/trackDb/trackDbDoc.js
+++ src/hg/htdocs/goldenPath/help/trackDb/trackDbDoc.js
@@ -1,954 +1,1003 @@
// 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('
Support level: ' + '' + level.replace('level-','') + '
');
}
$(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" + this + "";
});
}
}
// 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 = "" +id.replace(/_/g," ") + ""
//if (td.length > 1)
// best += " found "+td.length;
}
}
var row = "| " + aClass + " | ";
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 += "" +level.replace('level-','') + " | ";
}
row += "" + types + " | " + best + " |
";
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( ""+
"Table of Contents |
" );
}
$(tocTable).append("");
if ($(tocTable).find('th').length === 0) {
var th = "| Setting | ";
if (tdbDoc.isHubDoc()) {
th += "Level | ";
}
th += "For Types | Documented |
";
$(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("");
// 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';
}
},
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);
}
},
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);
}
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 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
+ 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
+ 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 regex = new RegExp(`(${escapedTerm})`, "gi");
+ let regexFactory = () => new RegExp(`(${escapedTerm})`, "gi");
+
+ // track processed text nodes so they don't get re-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
+ 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;
- if (regex.test(text)) {
+
+ // 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();
+
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';
}
}