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('<a class="button" HREF="'+url+'" TARGET=_blank><button>Download File</button></a>&nbsp;');
      w.document.write('<button id="closeWindowLink" HREF="#">Close Tab</button>');
      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('<pre>' + text + '</pre>'); // 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 <input> 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("<li class='ui-autocomplete-category'>" +
 +                                item.category + "</li>" );
 +                        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, '<b>$1</b>');
 +                   });
 +                }
 +                return $("<li></li>")
 +                    .data("ui-autocomplete-item", item)
 +                    .append($("<a></a>").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);
+ }
+