16eee40c920d259c10ee345472708d0cc0cc3393
chmalee
  Tue Mar 3 11:25:23 2026 -0800
Adds an hg.conf defined 'popular' species list to the new search bar as a 'default' list of results upon focus of the search bar. Combines with 'recents' list. Add a chevron next to the search bar so users know the autocomplete has some default options, refs #36232

diff --git src/hg/js/utils.js src/hg/js/utils.js
index 65166d34acb..f08bf39e019 100644
--- src/hg/js/utils.js
+++ src/hg/js/utils.js
@@ -4470,57 +4470,94 @@
     window.localStorage.setItem("recentGenomes", JSON.stringify(recentObj));
 }
 
 function getRecentGenomes() {
     // Retrieve recent genome selections from localStorage, formatted for autocomplete display.
     // Preserves original category for re-selection logic, adds displayCategory for UI.
     let stored = window.localStorage.getItem("recentGenomes");
     if (!stored) return [];
 
     let recentObj = JSON.parse(stored);
     let results = [];
     for (let genome of recentObj.stack) {
         if (recentObj.results[genome]) {
             let item = Object.assign({}, recentObj.results[genome]);
             // Preserve original category for setDbFromAutocomplete to detect GenArk/hubs
-            // but also provide a display category for UI
+            item.originalCategory = item.category;
+            // Set category for autocomplete grouping header
+            item.category = "Recent";
             item.displayCategory = "Recent";
             results.push(item);
         }
     }
     return results;
 }
 
+function getPopularGenomes($inputEl) {
+    // Parse the popular assemblies JSON embedded in the page by printGenomeSearchBar().
+    // Returns an array of autocomplete-formatted items with category "Popular".
+    // Items already present in recents are excluded to avoid duplicates.
+    let inputId = $inputEl.attr('id');
+    let dataEl = document.getElementById(inputId + 'PopularData');
+    if (!dataEl) return [];
+    let popularData;
+    try {
+        popularData = JSON.parse(dataEl.textContent);
+    } catch (e) {
+        return [];
+    }
+    // Build a set of db names already in recents to avoid duplicates
+    let recentDbs = new Set();
+    let recents = getRecentGenomes();
+    for (let r of recents) {
+        recentDbs.add(r.db || r.genome);
+    }
+    let results = [];
+    for (let p of popularData) {
+        if (recentDbs.has(p.db)) continue;
+        results.push({
+            genome: p.db,
+            db: p.db,
+            label: p.label,
+            commonName: p.commonName,
+            category: "Popular",
+            displayCategory: "Popular"
+        });
+    }
+    return results;
+}
+
 function removeRecentGenomesByHubUrl(hubUrl) {
     // Remove all recent genome entries whose hubUrl matches the given URL.
     // Used when a hub is disconnected to clean up stale entries.
     let stored = window.localStorage.getItem("recentGenomes");
     if (!stored) return;
     let recentObj = JSON.parse(stored);
     let newStack = [];
     for (let key of recentObj.stack) {
         let item = recentObj.results[key];
         if (item && item.hubUrl === hubUrl) {
             delete recentObj.results[key];
         } else {
             newStack.push(key);
         }
     }
     recentObj.stack = newStack;
     window.localStorage.setItem("recentGenomes", JSON.stringify(recentObj));
 }
 
+
 function recentGenomeHref(res) {
     // Build an hgTracks URL for a recent genome entry. GenArk assemblies need
     // db=, genome=, and a fully qualified hubUrl= so that fixUpDb() and
     // hubConnectLoadHubs() in cartNew() can connect the hub. UCSC native
     // databases just need db=.
     let db = res.db || res.genome;
     let url = new URL("../cgi-bin/hgTracks", window.location.href);
     url.searchParams.set("hgsid", getHgsid());
     url.searchParams.set("db", db);
     url.searchParams.set("position", "lastDbPos");
     if (res.hubUrl) {
         // The findGenome API returns a relative hubUrl (e.g. GCA/.../hub.txt)
         // while hgGateway stores it already prefixed (e.g. /gbdb/genark/GCA/.../hub.txt).
         let hubUrl = res.hubUrl;
         if (!hubUrl.startsWith("/gbdb/genark/")) {
@@ -4903,45 +4940,66 @@
  *   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
+        if (labelElement)
             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);
             });
         }
+
+        // Dropdown toggle button: opens/closes the autocomplete with recent+popular
+        let toggle = document.getElementById(config.inputId + "Toggle");
+        if (toggle) {
+            let wasOpen = false;
+            toggle.addEventListener("mousedown", () => {
+                let $input = $("[id='" + config.inputId + "']");
+                wasOpen = $input.autocompleteCat("widget").is(":visible");
+            });
+            toggle.addEventListener("click", () => {
+                let $input = $("[id='" + config.inputId + "']");
+                if (wasOpen) {
+                    $input.autocompleteCat("close");
+                } else {
+                    $input.val("");
+                    $input.autocompleteCat("search", "");
+                    $input.focus();
+                }
+            });
+        }
     });
 }
 
 function capitalizeFirstLetter(string) {
   return string.charAt(0).toUpperCase() + string.slice(1);
 }