e08ae97dc288749c2454fd98a10dc79f06e27cb5
max
Tue Aug 11 06:16:20 2026 -0700
hgBlat/hgc: fixes from the v502 code review of the BLAT results pages, refs #37893
#4 blatOldTracks now falls back to "keep" (with a warning) on any value other
than keep/hide/delete, instead of dropping into the destructive delete branch,
so a typo in hg.conf can never silently discard a user's earlier BLAT tracks.
#5 htmlEncode() (js/utils.js) now also escapes " and ', which the browser's
text->markup conversion leaves alone. Every hgBlat caller puts the result in a
double-quoted attribute, so an unescaped quote in the cart position string could
break out of the attribute; the shared helper now honors its documented contract.
#6 The non-BLAT alignment title no longer starts with a stray space.
#7 open_memstream() is checked for NULL: on that failure the alignment renders
straight to stdout instead of writing to a NULL FILE and calling fclose(NULL).
The section reorder keys on the literal "
// (this means that factor is currently the third sort column and reverse sorted)
// NOTE: If no TH.sortable is found, then every th in the TR.sortable will be
// converted for you and will be in sort1,2,3 order.)
// ONCLICK: Each TH.sortable must call sortTable.sortOnButtonPress(this) directly or
// indirectly in the onclick event. e.g.:
//
// NOTE: onclick function will automatically be added if not found.
// SUP: Each TH.sortable *may* contain a which will be filled with an up or down
// arrow and the column's sort order: e.g. ↓2
// NOTE: If no sup is found but addSuperscript is requested, then they will be added.
// TBODY.sortable: (NOTE: created if not found) The TBODY class='sortable' contains the
// table rows that get sorted:
// TBODY->TR & ->TD: Each row contains a TD for each sortable column.
// The innerHTML (entire contents) of the cell will be used for sorting.
// TRICK: You can use the 'abbr' field to subtly alter the sortable contents.
// Otherwise sorts on td contents ($(td).text()). Use the abbr field to make
// case-insensitive sorts or force exceptions to alpha-text order
// (such as ZCTRL vs Control forcing controls to bottom). e.g.:
//
'); // Display the content
w.document.close(); // Close the document to finish rendering
})
.catch(error => console.error('Error fetching BED file:', error));
}
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, onFilterDropdown = 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');
});
}
let $li = $("")
.data("ui-autocomplete-item", item)
.append($("").html(label))
.appendTo(ul);
if (item.disabled) {
$li.attr('title', item.disabledReason || 'Not available');
$li.css({'opacity': '0.5'});
$li.find('a').css({'pointer-events': 'none'});
}
return $li;
}
}
);
autocompleteCat.init($("[id='"+inputId+"']"), {
baseUrl: baseUrl !== null ? baseUrl : defaultSearchUrl,
watermark: watermark,
onSelect: selectFunction,
onServerReply: onServerReply !== null ? onServerReply : processFindGenome,
onError: onError,
onFilterDropdown: onFilterDropdown,
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
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);
}
// check whether an autocomplete item is for a genark assembly
function isGenarkItem(item) {
return (typeof item.hubUrl !== "undefined" && item.hubUrl) &&
(typeof item.genome !== "undefined" && item.genome) &&
(item.genome.startsWith('GCA_') || item.genome.startsWith('GCF_'));
}