68fcf0d16d6408ac16200557b687224bd86277e0 angie Wed Apr 13 16:24:49 2016 -0700 Refactored autocompleteCat options so that species autocomplete can do even more customized post-processing of results from server (insert phylo tree matches between dbDb matches and assembly hub matches). refs #15277 diff --git src/hg/js/hgGateway.js src/hg/js/hgGateway.js index 1b93dae..2fde271 100644 --- src/hg/js/hgGateway.js +++ src/hg/js/hgGateway.js @@ -636,121 +636,85 @@ } 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 return $("<li></li>") .data("item.autocomplete", item) .append($("<a></a>").html(item.label)) .appendTo(ul); } }); - function removeDups(inList, isDup) { - // Return a list with only unique items from inList, using isDup(a, b) -> true if a =~ b - var inLength = inList.length; - // inListDups is an array of boolean flags for marking duplicates, parallel to inList. - var inListDups = []; - var outList = []; - var i, j; - for (i = 0; i < inLength; i++) { - // If something has already been marked as a duplicate, skip it. - if (! inListDups[i]) { - // the first time we see a value, add it to outList. - outList.push(inList[i]); - for (j = i+1; j < inLength; j++) { - // Now scan the rest of inList to find duplicates of inList[i]. - // We can skip items previously marked as duplicates. - if (!inListDups[j] && isDup(inList[i], inList[j])) { - inListDups[j] = true; - } - } - } - } - return outList; - } - function init($input, options) { // Set up an autocomplete and watermark for $input, with a callback options.onSelect // for when the user chooses a result. // If options.baseUrl is null, the autocomplete will not do anything, but we (re)initialize // it anyway in case the same input had a previous db's autocomplete in effect. - // If options.searchObj is provided, it is used in addition to baseUrl; first the term is - // looked up in searchObj and then also queried using baseUrl. Values in searchObj - // should have the same structure as the value returned by a baseUrl query. - // options.isDuplicate (if provided) is a function (a, b) -> boolean that returns - // true if autocomplete items a and b are redundant; it is used to remove duplicates - // from autocomplete results. + // options.onServerReply (if given) is a function (Array, term) -> Array that + // post-processes the list of items returned by the server before the list is + // passed back to autocomplete for rendering. // The following two options apply only when using our locally modified jquery-ui: // If options.enterSelectsIdentical is true, then if the user hits Enter in the text input // and their term has an exact match in the autocomplete results, that result is selected. // options.onEnterTerm (if provided) is a callback function (jqEvent, jqUi) invoked // when the user hits Enter, after handling enterSelectsIdentical. // The function closure allows us to keep a private cache of past searches. var cache = {}; var doSearch = function(term, acCallback) { // Look up term in searchObj and by sending an ajax request var timestamp = new Date().getTime(); var url = options.baseUrl + encodeURIComponent(term) + '&_=' + timestamp; - var searchObjResults = []; - _.forEach(options.searchObj, function(results, key) { - if (_.startsWith(key.toUpperCase(), term.toUpperCase())) { - searchObjResults = searchObjResults.concat(results); - } - }); $.getJSON(url) .done(function(results) { - var combinedResults = results.concat(searchObjResults); - // Optionally remove duplicates identified by options.isDuplicate - if (options.isDuplicate) { - combinedResults = removeDups(combinedResults, options.isDuplicate); + if (_.isFunction(options.onServerReply)) { + results = options.onServerReply(results, term); } - cache[term] = combinedResults; - acCallback(combinedResults); + cache[term] = results; + acCallback(results); }); // ignore errors to avoid spamming people on flaky network connections // with tons of error messages (#8816). }; var autoCompleteSource = function(request, acCallback) { // This is a callback for jqueryui.autocomplete: when the user types // a character, this is called with the input value as request.term and an acCallback // for this to return the result to autocomplete. // See http://api.jqueryui.com/autocomplete/#option-source var results = cache[request.term]; if (results) { acCallback(results); } else if (options.baseUrl) { doSearch(request.term, acCallback); } }; var autoCompleteSelect = function(event, ui) { // This is a callback for autocomplete to let us know that the user selected // a term from the list. See http://api.jqueryui.com/autocomplete/#event-select options.onSelect(ui.item); $input.blur(); }; // Provide default values where necessary: options.onSelect = options.onSelect || console.log; - options.searchObj = options.searchObj || {}; options.enterSelectsIdentical = options.enterSelectsIdentical || false; $input.autocompleteCat({ delay: 500, minLength: 2, source: autoCompleteSource, select: autoCompleteSelect, enterSelectsIdentical: options.enterSelectsIdentical, enterTerm: options.onEnterTerm }); if (options.watermark) { $input.Watermark(options.watermark, '#686868'); } } @@ -860,44 +824,40 @@ var kidObj = autocompleteFromTree(kid); // Clone kid's result list and add own label as category: var kidResults = _.map(kidObj[kidLabel], addMyLabel); // Add kid's mappings to searchObj only if kid is not a leaf. if (kidKids && kidKids.length > 0) { _.assign(searchObj, kidObj); } return kidResults; }) ); } // Exclude some overly broad categories: if (label !== 'root' && label !== 'cellular organisms') { searchObj[label] = myResults; } - return searchObj; - } - - function addAutocompleteCommonNames(searchObj) { - // After searchObj is constructed by autocompleteFromTree, add aliases for - // some common names that map to scientific names in the tree. + // Add aliases for some common names that map to scientific names in the tree. _.forEach(commonToSciNames, function(sciName, commonName) { var label, addMyLabel; if (searchObj[sciName]) { label = sciName + ' (' + commonName + ')'; addMyLabel = addCategory.bind(null, label); searchObj[commonName] = _.map(searchObj[sciName], addMyLabel); } }); + return searchObj; } function makeStripe(id, color, stripeHeight, scrollTop, onClickStripe) { // Return an empty div with specified background color and height var $stripe = $('<div class="jwRainbowStripe">'); $stripe.attr('id', 'rainbowStripe' + id); $stripe.attr('title', 'Click to scroll the tree display'); $stripe.css('background-color', color); $stripe.height(stripeHeight); $stripe.click(onClickStripe.bind(null, scrollTop)); return $stripe; } function makeRainbowSliderStripes($slider, onClickStripe, svgHeight, stripeColors, stripeTops) { // Set up the rainbow slider bar for the speciesPicker. @@ -1312,41 +1272,92 @@ setAssemblyOptions(uiState); if (uiState.position) { $('#positionDisplay').text(uiState.position); } autocompleteCat.init($('#positionInput'), { baseUrl: suggestUrl, watermark: positionWatermark, onSelect: onSelectGene, enterSelectsIdentical: true, onEnterTerm: goToHgTracks }); updateGoButtonPosition(); setAssemblyDescriptionTitle(uiState.db, uiState.genome); updateDescription(uiState.description); } + function removeDups(inList, isDup) { + // Return a list with only unique items from inList, using isDup(a, b) -> true if a =~ b + var inLength = inList.length; + // inListDups is an array of boolean flags for marking duplicates, parallel to inList. + var inListDups = []; + var outList = []; + var i, j; + for (i = 0; i < inLength; i++) { + // If something has already been marked as a duplicate, skip it. + if (! inListDups[i]) { + // the first time we see a value, add it to outList. + outList.push(inList[i]); + for (j = i+1; j < inLength; j++) { + // Now scan the rest of inList to find duplicates of inList[i]. + // We can skip items previously marked as duplicates. + if (!inListDups[j] && isDup(inList[i], inList[j])) { + inListDups[j] = true; + } + } + } + } + return outList; + } + function speciesResultsEquiv(a, b) { // For autocompleteCat's option isDuplicate: return true if species search results // a and b would be redundant (and hence one should be removed). if (a.db !== b.db) { return false; } else if (a.genome === b.genome) { return true; } return false; } + function searchByKeyNoCase(searchObj, term) { + // Return a concatenation of searchObj list values whose keys start with term + // (case-insensitive). + var termUpCase = term.toUpperCase(); + var searchObjResults = []; + _.forEach(searchObj, function(results, key) { + if (_.startsWith(key.toUpperCase(), termUpCase)) { + searchObjResults = searchObjResults.concat(results); + } + }); + return searchObjResults; + } + + function processSpeciesAutocompleteItems(searchObj, results, term) { + // This (bound to searchObj) is passed into autocompleteCat as options.onServerReply. + // The server sends a list of items that may include duplicates and can have + // results from dbDb and/or assembly hubs. Also look for results from the + // phylogenetic tree, and insert those before the assembly hub matches. + // Then remove duplicates and return the processed results which will then + // be used to render the menu. + var phyloResults = searchByKeyNoCase(searchObj, term); + var hubResultIx = _.findIndex(results, function(result) { return !! result.hubUrl; }); + var hubResults = hubResultIx >= 0 ? results.splice(hubResultIx) : []; + var combinedResults = results.concat(phyloResults).concat(hubResults); + return removeDups(combinedResults, speciesResultsEquiv); + } + // Server response event handlers function checkJsonData(jsonData, callerName) { // Return true if jsonData isn't empty and doesn't contain an error; // otherwise complain on behalf of caller. if (! jsonData) { alert(callerName + ': empty response from server'); } else if (jsonData.error) { console.error(jsonData.error); alert(callerName + ': error from server: ' + jsonData.error); } else { if (debugCartJson) { console.log('from server:\n', jsonData); } return true; @@ -1550,41 +1561,40 @@ }); } function init() { // Boot up the page; initialize elements and install event handlers. cart.setCgi('hgGateway'); cart.debug(debugCartJson); // Get state from cart cart.send({ getUiState: {} }, handleRefreshState); cart.flush(); // When page has loaded, draw the species tree, do layout adjustments and // initialize event handlers. $(function() { var searchObj = autocompleteFromTree(dbDbTree); - addAutocompleteCommonNames(searchObj); + var processSpeciesResults = processSpeciesAutocompleteItems.bind(null, searchObj); scrollbarWidth = findScrollbarWidth(); drawSpeciesPicker(); setRightColumnWidth(); setupFavIcons(); autocompleteCat.init($('#speciesSearch'), { baseUrl: 'hgGateway?hggw_term=', watermark: speciesWatermark, onSelect: setDbFromAutocomplete, - searchObj: searchObj, - isDuplicate: speciesResultsEquiv, + onServerReply: processSpeciesResults, enterSelectsIdentical: true }); updateFindPositionSection(uiState); $('#selectAssembly').change(onChangeDbMenu); $('#positionDisplay').click(onClickCopyPosition); $('#copyPosition').click(onClickCopyPosition); $('.jwGoButtonContainer').click(goToHgTracks); $(window).resize(setRightColumnWidth.bind(null, scrollbarWidth)); $(window).resize(updateGoButtonPosition); replaceHgsidInLinks(); }); } return { init: init, // For use by speciesTree.draw SVG (text-only onclick): onClickSpeciesLabel: onClickSpeciesLabel,