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> '); 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); + } +