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 += "&nbsp;" + 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'>&nbsp;Show 490 (out of " + (matches.length) + " total) more matches for " + searchDesc + "</div></li>";
-                    else
-                        subList.innerHTML += "<div class='showMoreDiv' id='" + idAttr.value+"_"+categoryCount+"_showMoreDiv'>&nbsp;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 += "&nbsp;<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 += "&nbsp;<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