4a5e33685ee92a55d85200b621eb992d4c48cbfe chmalee Tue Nov 14 11:46:11 2023 -0800 Make the Show/Hide results text on hgSearch a link that triggers the showing or hiding of results. Also fix up incorrect comparisons to undefined and use the same linter settings as the rest of the javascript files, refs #31501 diff --git src/hg/js/hgSearch.js src/hg/js/hgSearch.js index d5b6680..fcab8c4 100644 --- src/hg/js/hgSearch.js +++ src/hg/js/hgSearch.js @@ -1,15 +1,23 @@ +// Utility JavaScript + +// "use strict"; + +// Don't complain about line break before '||' etc: +/* jshint -W014 */ +/* jshint -W087 */ +/* jshint esnext: true */ var db; /* A global variable for the current assembly. Needed when we may not have sent a cartJson request yet */ var hgSearch = (function() { // this object contains everything needed to build current state of the page var uiState = { db: "", /* The assembly for which all this business belongs to */ categs: {}, /* all possible categories for this database, this includes all possible * searchable tracks */ currentCategs: {}, /* the categories (filters) for the current search results */ positionMatches: [], /* an array of search result objects, one for each category, * created by hgPositionsJson in cartJson.c */ search: "", /* what is currently in the search box */ trackGroups: {}, /* the track groups available for this assembly */ resultHash: {}, /* positionMatches but each objects' category name is a key */ @@ -97,31 +105,31 @@ function changeUrl(vars, oldVars) { /* Save the users search string to the url so web browser can easily * cache search results into the browser history * vars: object of new key: val pairs like CGI arguments * oldVars: arguments we want to keep between calls */ var myUrl = window.location.href; myUrl = myUrl.replace('#',''); var urlParts = myUrl.split("?"); var baseUrl; if (urlParts.length > 1) baseUrl = urlParts[0]; else baseUrl = myUrl; var urlVars; - if (oldVars === undefined) { + if (typeof oldVars === "undefined") { var queryStr = urlParts[1]; urlVars = deparam(queryStr); } else { urlVars = oldVars; } for (var key in vars) { var val = vars[key]; if (val === null || val === "") { if (key in urlVars) { delete urlVars[key]; } } else { urlVars[key] = val; } @@ -148,79 +156,79 @@ * followed by short label */ priorityA = trackA.priority; priorityB = trackB.priority; // if both priorities are undefined or equal to each other, sort // on shortlabel alphabetically if (priorityA === priorityB) { if (trackA.name < trackB.name) { return -1; } else if (trackA.name > trackB.name) { return 1; } else { return 0; } } else { - if (priorityA === undefined) { + if (typeof priorityA === "undefined") { return 1; - } else if (priorityB === undefined) { + } else if (typeof priorityB === "undefined") { return -1; } else if (priorityA < priorityB) { return -1; } else if (priorityA > priorityA) { return 1; } else { return 0; } } } function compareGroups(a, b) { /* Compare function for track group sorting */ return uiState.trackGroups[a.name].priority - uiState.trackGroups[b.name].priority; } function sortByTrackGroups(groupList) { return groupList.sort(compareGroups); } function sortTrackCategories(trackList) { /* Sort the nested track list structure such that within each group * the leaves of the tree are sorted by priority */ - if (trackList.children !== undefined) { + if (typeof trackList.children !== "undefined") { trackList.children.sort(compareTrack); for (var i = 0; i < trackList.children.length; i++) { trackList.children[i] = sortTrackCategories(trackList.children[i]); } } return trackList; } function categoryComp(category) { - if (category.priority !== undefined) + if (typeof category.priority !== "undefined") return category.priority; return 1000.0; } function sortCategories(categList) { return _.sortBy(categList, categoryComp); } function addCountAndTimeToLabel(categ) { /* Change the text label of the node */ categ.text = categ.label + " (<span id='" + categ.id + "count'><b>" + categ.numMatches + " results</b></span>"; - if (categ.searchTime !== undefined && categ.searchTime >= 0) { + if (typeof categ.searchTime !== "undefined" && categ.searchTime >= 0) { categ.text += ", <span id='" + categ.id + "searchTime'><b>" + categ.searchTime + "ms searchTime</b></span>"; } categ.text += ")"; } function tracksToTree(trackList) { /* Go through the list of all tracks for this assembly, filling * out the necessary information for jstree to be able to work. * Only include categories that have results. * The groups object will get filled out along the way. */ trackGroups = uiState.trackGroups; var ret = []; var parentsHash = {}; var groups = {}; visibleTrackGroup.children = []; @@ -237,120 +245,120 @@ _.assign(newCateg, track); newCateg.text = track.longLabel; newCateg.text = track.label; var group = track.group; newCateg.state = {checked: true, opened: true}; newCateg.text = track.label; newCateg.li_attr = {title: track.description}; newCateg.numMatches = uiState.resultHash[newCateg.id].matches.length; newCateg.searchTime = uiState.resultHash[newCateg.id].searchTime; addCountAndTimeToLabel(newCateg); if (track.visibility > 0) { if (!groups.visible) { groups.visible = visibleTrackGroup; } groups.visible.children.push(newCateg); - if (newCateg.searchTime !== undefined) { + if (typeof newCateg.searchTime !== "undefined") { if (groups.visible.searchTime < 0) groups.visible.searchTime = 0; groups.visible.searchTime += newCateg.searchTime; } groups.visible.numMatches += newCateg.numMatches; } else { var last = newCateg; var doNewComp = true; if (track.parents) { var tracksAndLabels = track.parents.split(','); var l = tracksAndLabels.length; for (var i = 0; i < l; i+=2) { var parentTrack= tracksAndLabels[i]; var parentLabel = tracksAndLabels[i+1]; if (!(parentTrack in parentsHash)) { parent = {}; parent.id = parentTrack; parent.label = parentLabel; parent.children = [last]; parent.li_attr = {title: "Search for track items in all of the searchable subtracks of the " + parentLabel + " track"}; parent.numMatches = last.numMatches; parent.searchTime = last.searchTime; parentsHash[parentTrack] = parent; addCountAndTimeToLabel(parent); last = parent; doNewComp = true; - } else if (last !== undefined) { + } else if (typeof last !== "undefined") { // if we are processing the first parent, we need to add ourself (last) // as a child so the subtrack list is correct, but we still need // to go up through the parent list and update the summarized counts if (doNewComp) { parentsHash[parentTrack].children.push(last); } doNewComp = false; parentsHash[parentTrack].numMatches += last.numMatches; - if (last.searchTime !== undefined) { + if (typeof last.searchTime !== "undefined") { parentsHash[parentTrack].searchTime += last.searchTime; } addCountAndTimeToLabel(parent); } } } - if (groups[group] !== undefined && last !== undefined) { + if (typeof groups[group] !== "undefined" && typeof last !== "undefined") { groups[group].numMatches += last.numMatches; - if (last.searchTime !== undefined) { + if (typeof last.searchTime !== "undefined") { groups[group].searchTime += last.searchTime; } addCountAndTimeToLabel(groups[group]); if (doNewComp) { groups[group].children.push(last); } } else if (doNewComp) { groups[group] = {}; groups[group].id = group; groups[group].name = group; groups[group].label = group; addCountAndTimeToLabel(groups[group]); groups[group].numMatches = last.numMatches; groups[group].searchTime = last.searchTime; groups[group].children = [last]; - if (trackGroups !== undefined && group in trackGroups) { + if (typeof trackGroups !== "undefined" && group in trackGroups) { groups[group].priority = trackGroups[group].priority; groups[group].label = trackGroups[group].label; addCountAndTimeToLabel(groups[group]); } else { trackGroups[group] = groups[group]; } } } }); if ("visible" in groups) { groups.visible.children = sortTrackCategories(groups.visible.children); addCountAndTimeToLabel(groups.visible); ret.push(groups.visible); } hiddenTrackChildren = []; _.each(groups, function(group) { if (group.id !== "Visible Tracks") { group.li_attr = {title: "Search for track items in the " + group.label+ " set of tracks"}; group.state = {checked: true, opened: true}; group.children = sortTrackCategories(group.children); hiddenTrackChildren.push(group); } }); if (hiddenTrackChildren.length > 0) { hiddenTrackChildren = sortByTrackGroups(hiddenTrackChildren); _.each(hiddenTrackChildren, function(group) { hiddenTrackGroup.children.push(group); - if (group.searchTime !== undefined) { + if (typeof group.searchTime !== "undefined") { if (hiddenTrackGroup.searchTime < 0) { hiddenTrackGroup.searchTime = 0; } hiddenTrackGroup.searchTime += group.searchTime; } hiddenTrackGroup.numMatches += group.numMatches; }); addCountAndTimeToLabel(hiddenTrackGroup); ret.push(hiddenTrackGroup); } return ret; } function filtersToJstree() { /* Turns uiState.categs into uiState.currentCategs, which populates the @@ -397,61 +405,61 @@ return thisCategs[ele]; })) }; } function showOrHideResults(event, node) { /* When a checkbox is checked/uncheck in the tree, show/hide the corresponding * result section in the list of results */ var state = node.state.checked; if (node.children.length > 0) { _.each(node.children, function(n) { showOrHideResults(event, $("#searchCategories").jstree().get_node(n)); }); } else { resultLi = $('[id="' + node.id + 'Results"'); - if (resultLi !== undefined) // if we don't have any results for this track resultLi is undefined + if (typeof resultLi !== "undefined") // if we don't have any results for this track resultLi is undefined _.each(resultLi, function(li) { li.style = state ? "display" : "display: none"; }); } } function buildTree(node, cb) { cb.call(this, uiState.currentCategs[node.id]); } function makeCategoryTree() { var parentDiv = $("#searchCategories"); $.jstree.defaults.core.themes.icons = false; $.jstree.defaults.core.themes.dots = true; $.jstree.defaults.contextmenu.show_at_node = false; parentDiv.jstree({ 'plugins' : ['contextmenu', 'checkbox'], 'core': { 'data': buildTree, 'check_callback': true }, 'checkbox': { 'tie_selection': false } }); parentDiv.css('height', "auto"); } function updateFilters(uiState) { - if (uiState.categs !== undefined) { + if (typeof uiState.categs !== "undefined") { filtersToJstree(); makeCategoryTree(); } } function clearOldFacetCounts() { $("[id*='extraInfo']").remove(); } function printMatches(list, matches, title, searchDesc) { var printCount = 0; _.each(matches, function(match, printCount) { var position = match.position.split(':'); var url, matchTitle; if (title === "helpDocs") { @@ -522,58 +530,69 @@ } }); } function showMoreResults() { var trackName = this.id.replace(/Results_.*/, ""); var isHidden = $("." + trackName + "_hidden")[0].style.display === "none"; _.each($("." + trackName + "_hidden"), function(hiddenLi) { if (isHidden) { hiddenLi.style = "display:"; } else { hiddenLi.style = "display: none"; } }); if (isHidden) { + if (this.nextSibling) { + // click on the '+' icon newText = this.nextSibling.innerHTML.replace(/Show/,"Hide"); this.nextSibling.innerHTML = newText; this.src = "../images/remove_sm.gif"; } else { + // click on the link text + this.innerHTML = this.innerHTML.replace(/Show/,"Hide"); + } + } else { + if (this.nextSibling) { + // click on the '-' icon newText = this.nextSibling.innerHTML.replace(/Hide/,"Show"); this.nextSibling.innerHTML = newText; this.src = "../images/add_sm.gif"; + } else { + this.innerHTML = this.innerHTML.replace(/Hide/,"Show"); + } } } function collapseNode() { var toCollapse = this.parentNode.childNodes[3]; var isHidden = toCollapse.style.display === "none"; if (isHidden) { toCollapse.style = 'display:'; this.src = "../images/remove_sm.gif"; } else { toCollapse.style = 'display: none'; this.src = "../images/add_sm.gif"; } } function updateSearchResults(uiState) { var parentDiv = $("#searchResults"); - if (uiState && uiState.search !== undefined) { + if (uiState && typeof uiState.search !== "undefined") { $("#searchBarSearchString").val(uiState.search); } else { // back button all the way to the beginning $("#searchBarSearchString").val(""); } if (uiState && uiState.positionMatches && uiState.positionMatches.length > 0) { // clear the old search results if there were any: parentDiv.empty(); // create the elements that will hold results: var newList = document.createElement("ul"); var noUlStyle = document.createAttribute("class"); noUlStyle.value = "ulNoStyle"; newList.setAttributeNode(noUlStyle); parentDiv.append(newList); @@ -589,74 +608,85 @@ var newListObj = document.createElement("li"); var idAttr = document.createAttribute("id"); idAttr.value = title + 'Results'; newListObj.setAttributeNode(idAttr); var noLiStyle = document.createAttribute("class"); noLiStyle.value = "liNoStyle"; newListObj.setAttributeNode(noLiStyle); newListObj.innerHTML += "<input type='hidden' id='" + idAttr.value + categoryCount + "' value='0'>"; newListObj.innerHTML += "<img height='18' width='18' id='" + idAttr.value + categoryCount + "_button' src='../images/remove_sm.gif'>"; newListObj.innerHTML += " " + searchDesc + ":"; //printOneFullMatch(newList, matches[0], title, searchDesc); // Now loop through each actual hit on this table and unpack onto list var subList = document.createElement("ul"); printMatches(subList, matches, title, searchDesc); if (matches.length > 10) { + idStr = idAttr.value + "_" + categoryCount; subList.innerHTML += "<li class='liNoStyle'>"; - subList.innerHTML += "<input type='hidden' id='" + idAttr.value + "_" + categoryCount + "showMore' value='0'>"; - subList.innerHTML += "<img height='18' width='18' id='" + idAttr.value + "_" + categoryCount + "_showMoreButton' src='../images/add_sm.gif'>"; - if (matches.length > 500) - subList.innerHTML += "<div class='showMoreDiv' id='" + idAttr.value+"_"+categoryCount+"_showMoreDiv'> Show 490 (out of " + (matches.length) + " total) more matches for " + searchDesc + "</div></li>"; - else - subList.innerHTML += "<div class='showMoreDiv' id='" + idAttr.value+"_"+categoryCount+"_showMoreDiv'> Show " + (matches.length - 10) + " more matches for " + searchDesc + "</div></li>"; + subList.innerHTML += "<input type='hidden' id='" + idStr + "showMore' value='0'>"; + subList.innerHTML += "<img height='18' width='18' id='" + idStr + "_showMoreButton' src='../images/add_sm.gif'>"; + if (matches.length > 500) { + let newText = "<div class='showMoreDiv' id='" + idStr +"_showMoreDiv'>"; + newText += " <a href='#' id='"+ idStr + "_showMoreLink'>"; + newText += "Show 490 (out of " + (matches.length) + " total) more matches for " + searchDesc; + newText += "</a></div></li>"; + subList.innerHTML += newText; + } else { + let newText = "<div class='showMoreDiv' id='" + idStr + "_showMoreDiv'>"; + newText += " <a href='#' id='"+ idStr + "_showMoreLink'>"; + newText += "Show " + (matches.length - 10) + " more matches for " + searchDesc; + newText += "</a></div></li>"; + subList.innerHTML += newText; + } } newListObj.append(subList); newList.append(newListObj); // make result list collapsible: $('#'+idAttr.value+categoryCount+"_button").click(collapseNode); $('#'+idAttr.value+"_" +categoryCount+"_showMoreButton").click(showMoreResults); + $('#'+idAttr.value + "_" + categoryCount + "_showMoreLink").click(showMoreResults); categoryCount += 1; }); - } else if (uiState && uiState.search !== undefined) { + } else if (uiState && typeof uiState.search !== "undefined") { // No results from match var msg = "<p>No results</p>"; parentDiv.empty(); parentDiv.html(msg); clearOldFacetCounts(); } else { parentDiv.empty(); } } function fillOutAssemblies(e) { organism = $("#speciesSelect")[0].value; select = $("#dbSelect"); select.empty(); _.each(_.sortBy(uiState.genomes[organism], ['orderKey']), function(assembly) { newOpt = document.createElement("option"); newOpt.value = assembly.name; newOpt.label = trackHubSkipHubName(assembly.organism) + " " + assembly.description; if (assembly.name == db) { newOpt.selected = true; } $("#dbSelect").append(newOpt); }); // if we are getting here from a change event on the species dropdown // and are switching to the default assembly for a species, we can // automatically send a search for this organism+assembly - if (e !== undefined) { + if (typeof e !== "undefined") { switchAssemblies($("#dbSelect")[0].value); } } function buildSpeciesDropdown() { // Process the species select dropdowns _.each(uiState.genomes, function(genome) { newOpt = document.createElement("option"); newOpt.value = genome[0].organism; newOpt.label = trackHubSkipHubName(genome[0].organism); if (genome.some(function(assembly) { if (assembly.isCurated) { if (assembly.name === trackHubSkipHubName(db)) { return true; } @@ -690,31 +720,36 @@ alert("Warning: " + jsonData.warning); return true; } else { if (debugCartJson) { console.log('from server:\n', jsonData); } return true; } return false; } function updateStateAndPage(jsonData, doSaveHistory) { // Update uiState with new values and update the page. _.assign(uiState, jsonData); db = uiState.db; - if (jsonData.positionMatches !== undefined) { + if (typeof jsonData === "undefined" || jsonData === null) { + // now that the show more text is a link, a popstate event gets fired because the url changes + // we can safely return because there is no state to change + return; + } + if (typeof jsonData.positionMatches !== "undefined") { // clear the old resultHash uiState.resultHash = {}; _.each(uiState.positionMatches, function(match) { uiState.resultHash[match.name] = match; }); } else { // no results for this search uiState.resultHash = {}; uiState.positionMatches = []; } updateFilters(uiState); updateSearchResults(uiState); buildSpeciesDropdown(); fillOutAssemblies(); urlVars = {"db": db, "search": uiState.search, "showSearchResults": ""}; @@ -731,31 +766,31 @@ updateStateAndPage(jsonData, true); } $("#spinner").remove(); } function handleErrorState(jqXHR, textStatus) { cart.defaultErrorCallback(jqXHR, textStatus); $("#spinner").remove(); } function sendUserSearch() { // User has clicked the search button, if they also entered a search // term, fire off a search cart.debug(debugCartJson); var searchTerm = $("#searchBarSearchString").val().replaceAll("\"",""); - if (searchTerm !== undefined && searchTerm.length > 0) { + if (typeof searchTerm !== 'undefined' && searchTerm.length > 0) { // put up a loading image $("#searchBarSearchButton").after("<i id='spinner' class='fa fa-spinner fa-spin'></i>"); // redirect to hgBlat if the input looks like a DNA sequence // minimum length=19 so we do not accidentally redirect to hgBlat for a gene identifier // like ATG5 var dnaRe = new RegExp("^(>[^\n\r ]+[\n\r ]+)?(\\s*[actgnACTGN \n\r]{19,}\\s*)$"); if (dnaRe.test(searchTerm)) { var blatUrl = "hgBlat?type=BLAT%27s+guess&userSeq="+searchTerm; window.location.href = blatUrl; return false; } // if the user entered a plain position string like chr1:blah-blah, just // go to the old cgi/hgTracks