20ade701f198a5dc8ba8ea9483f6c98666451142
Merge parents 9efc0d9b0b8 aee2779a0e1
max
Tue Feb 24 14:59:27 2026 -0800
Merge branch 'ui-supertrack'
diff --cc src/hg/js/utils.js
index 596622cb539,bbdee5d271a..451dc2ce9f6
--- src/hg/js/utils.js
+++ src/hg/js/utils.js
@@@ -4756,187 -4561,19 +4756,193 @@@
var w = window.open('');
w.document.write(' ');
w.document.write('');
w.onload = function(ev) {
// Attach event listeners after the new window is loaded
w.document.getElementById('closeWindowLink').addEventListener('click', function() { w.close(); } );
};
fetch(url).then(response => response.text()) // Read the response as text
.then(function(text) {
w.document.write('
' + text + '
'); // Display the content
w.document.close(); // Close the document to finish rendering
})
.catch(error => console.error('Error fetching BED file:', error));
}
++<<<<<<< HEAD
+function processFindGenome(result, term) {
+ // process the hubApi/findGenome?q= result set into somthing
+ // jquery-ui autocomplete can use
+ let data = [];
+ let apiSkipList = new Set(["downloadTime", "downloadTimeStamp", "availableAssemblies", "browser", "elapsedTimeMs", "itemCount", "q", "totalMatchCount", "liftable"]);
+ Object.keys(result).forEach((key) => {
+ if (!(apiSkipList.has(key))) {
+ let val = result[key];
+ let d = {
+ "genome": key,
+ "label": `${val.commonName} (${key})`,
+ };
+
+ Object.keys(val).forEach((vkey) => {
+ d[vkey] = val[vkey];
+ });
+ // Set db to the key (database name or accession) so the autocomplete
+ // select handler can save it to recent genomes
+ d.db = key;
+ if (val.hubUrl !== null) {
+ d.category = "UCSC GenArk - bulk annotated assemblies from NCBI GenBank / Refseq";
+ } else {
+ d.category = "UCSC Genome Browser assemblies - annotation tracks curated by UCSC";
+ }
+ data.push(d);
+ }
+ });
+ return data;
+}
+
+function initSpeciesAutoCompleteDropdown(inputId, selectFunction, baseUrl = null,
+ watermark = null, onServerReply = null, onError = null) {
+/* Generic function for turning an element into a species search bar with an autocomplete
+ * list separating results by category.
+ * Required arguments:
+ * inputId: id of the input element itself, not created here
+ * selectFunction: the function to call when the user actually clicks on a result
+ * Optional arguments:
+ * baseUrl: where we send requests to which will return json we can parse into a list
+ * of results, defaults to 'hubApi/findGenome?browser=mustExist&q='
+ * watermark: placeholder text in the input
+ * onServerReply: function to run after querying baseUrl, by default use processFindGenome()
+ * to standardize on hubApi, but can be something else
+ * onError: function to call when the server returns an error (e.g. HTTP 400)
+ * signature: onError(jqXHR, textStatus, errorThrown, searchTerm) => results array or null
+ */
+ let defaultSearchUrl = "hubApi/findGenome?browser=mustExist&q=";
+ $.widget("custom.autocompleteCat",
+ $.ui.autocomplete,
+ {
+ _renderMenu: function(ul, items) {
+ var that = this;
+ var currentCategory = "";
+ // There's no this._super as shown in the doc, so I can't override
+ // _create as shown in the doc -- just do this every time we render...
+ this.widget().menu("option", "items", "> :not(.ui-autocomplete-category)");
+ $(ul).css("z-index", "99999999");
+ // Check if all items are from recents (have displayCategory === "Recent")
+ // If so, skip category headers since they're all recent selections
+ var allRecent = items.length > 0 && items.every(function(item) {
+ return item.displayCategory === "Recent";
+ });
+ $.each(items, function(index, item) {
+ // Add a heading each time we see a new category (but not for recents)
+ if (!allRecent && item.category && item.category !== currentCategory) {
+ ul.append("
" +
+ item.category + "
" );
+ currentCategory = item.category;
+ }
+ that._renderItem( ul, item );
+ });
+ },
+ _renderItem: function(ul, item) {
+ // In order to use HTML markup in the autocomplete, one has to overwrite
+ // autocomplete's _renderItem method using .html instead of .text.
+ // http://forum.jquery.com/topic/using-html-in-autocomplete
+ // Hits to assembly hub top level (not individial db names) have no item label,
+ // so use the value instead
+ var searchTerm = this.term;
+ // remove special characters - the \W means remove anything
+ // that is not: [A-Za-z0-9_] which are 'word' == \w characters
+ // then eliminate runs of white space characters and trim any
+ // white space at the beginning or end of the string
+ var cleanTerm = searchTerm.replace(/\W/g, ' ')
+ .replace(/\s+/g, ' ')
+ .trim();
+ var label = item.label !== null ? item.label : item.value;
+
+ // Highlight matching search terms with bold tags
+ if (cleanTerm && cleanTerm.length > 0) {
+ // Split search term into individual words (by whitespace)
+ var words = cleanTerm.split(/\s+/).filter(function(word) {
+ return word.length > 0; // Filter out empty strings
+ });
+
+ // Apply bolding for each word separately
+ words.forEach(function(word) {
+ // Escape special regex characters in each word
+ var escapedWord = word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+
+ // Create case-insensitive regex to find all occurrences
+ var regex = new RegExp('(' + escapedWord + ')', 'gi');
+
+ // Replace matches with bolded version (preserves original case)
+ label = label.replace(regex, '$1');
+ });
+ }
+ return $("")
+ .data("ui-autocomplete-item", item)
+ .append($("").html(label))
+ .appendTo(ul);
+ }
+ }
+ );
+ autocompleteCat.init($("[id='"+inputId+"']"), {
+ baseUrl: baseUrl !== null ? baseUrl : defaultSearchUrl,
+ watermark: watermark,
+ onSelect: selectFunction,
+ onServerReply: onServerReply !== null ? onServerReply : processFindGenome,
+ onError: onError,
+ showRecentGenomes: true,
+ enterSelectsIdentical: false
+ });
+}
+
+function setupGenomeSearchBar(config) {
+/* Higher-level wrapper for setting up a genome search bar with standard boilerplate.
+ * This handles the common pattern used across CGIs: error handling, DOMContentLoaded,
+ * element binding, search button handler, item validation, and label update.
+ *
+ * config object properties:
+ * inputId (required): ID of the search input element
+ * labelElementId: ID of the element to update with selected genome label (default: 'genomeLabel')
+ * onSelect: Callback function(item, labelElement) when genome is selected.
+ * Called AFTER standard validation and label update.
+ * item has: {genome, label, commonName, disabled, ...}
+ * apiUrl: Custom API URL (default: null uses standard hubApi/findGenome)
+ * onServerReply: Custom function to process API results (default: null uses processFindGenome)
+ */
+ function onSearchError(jqXHR, textStatus, errorThrown, term) {
+ return [{label: 'No genomes found', value: '', genome: '', disabled: true}];
+ }
+
+ function wrappedSelect(labelElement, item) {
+ // Standard validation - all CGIs check this
+ if (item.disabled || !item.genome) return;
+ // Standard label update - all CGIs do this
+ labelElement.innerHTML = item.label;
+ // Call user's custom callback for CGI-specific logic
+ if (typeof config.onSelect === 'function') {
+ config.onSelect(item, labelElement);
+ }
+ }
+
+ document.addEventListener("DOMContentLoaded", () => {
+ let labelElementId = config.labelElementId || 'genomeLabel';
+ let labelElement = document.getElementById(labelElementId);
+ let boundSelect = wrappedSelect.bind(null, labelElement);
+
+ initSpeciesAutoCompleteDropdown(config.inputId, boundSelect,
+ config.apiUrl || null, null, config.onServerReply || null, onSearchError);
+
+ // Standard search button handler
+ let btn = document.getElementById(config.inputId + "Button");
+ if (btn) {
+ btn.addEventListener("click", () => {
+ let val = document.getElementById(config.inputId).value;
+ $("[id='" + config.inputId + "']").autocompleteCat("search", val);
+ });
+ }
+ });
+}
++
+ function capitalizeFirstLetter(string) {
+ return string.charAt(0).toUpperCase() + string.slice(1);
+ }
+