47ea57080b515e5dad5f658c58feb8944a7e7d61 chmalee Thu Jan 29 15:30:26 2026 -0800 Replace clade/assembly dropdowns with a search bar on most CGIs. Add a recents list to hgGateway and to the species bar and to the 'Genomes' dropdown menu. Track recently selected species in localStorage. Add toGenome and fromGenome arguemnts to hubApi/liftOver in order to find appropriate liftover assemblies, refs #36232 diff --git src/hg/js/react/lib/PositionSearch.jsx src/hg/js/react/lib/PositionSearch.jsx index e89593dc48d..2cb9c507aca 100644 --- src/hg/js/react/lib/PositionSearch.jsx +++ src/hg/js/react/lib/PositionSearch.jsx @@ -1,230 +1,231 @@ /** @jsx React.DOM */ /* global ImmutableUpdate, PathUpdate, Icon, Modal, TextInput */ var pt = React.PropTypes; var PositionPopup = null; // subcomponent, defined below var PositionSearch = React.createClass({ // Text input for position or search term, optionally with autocomplete if // props includes geneSuggestTrack, and a PositionPopup (defined below) // for multiple position/search matches. mixins: [PathUpdate, ImmutableUpdate], // update(path + 'position', newValue) called when user changes position // update(path + 'hidePosPopup') called when user clicks to hide popup // update(path + 'positionMatch', matches): user clicks position link in popup // (matches obj is from hgFind) propTypes: { positionInfo: pt.object.isRequired, // Immutable.Map { // position: initial value of position input // loading (bool): display spinner next to position input // positionMatches (Immutable.Vector of Maps): multiple search results for popup // geneSuggestTrack: optional track to use for autocomplete // } // Conditionally required db: pt.string, // must be given if positionInfo includes geneSuggestTrack // Optional className: pt.string // class(es) to pass to wrapper div }, autoCompleteSourceFactory: function() { // This returns a 'source' callback function for jqueryui.autocomplete. // We get a lot of duplicate requests (especially the first letters of // words), so we keep a cache of the suggestions lists we've retreived. var cache = {}; function makeUrl(db, key) { return '../cgi-bin/hgSuggest?db=' + db + '&prefix=' + encodeURIComponent(key); } return 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 if (this.props.positionInfo.get('geneSuggestTrack')) { var key = request.term; var db = this.props.db; if (cache[db] && cache[db][key]) { acCallback(cache[db][key]); } else { var url = makeUrl(db, key); $.getJSON(url) .done(function(result) { cache[db] = cache[db] || {}; cache[db][key] = result; acCallback(result); }); // ignore errors to avoid spamming people on flaky network connections // with tons of error messages (#8816). } } }.bind(this); }, autoCompleteMenuOpen: function() { // This is an 'open' event callback for autocomplete to let us know when the // menu showing completions is opened. // See http://api.jqueryui.com/autocomplete/#event-open // Determine whether the menu will need a scrollbar: var $jq = $(this.refs.input.getDOMNode()); var pos = $jq.offset().top + $jq.height(); if (!isNaN(pos)) { // take off a little more because IE needs it: var maxHeight = $(window).height() - pos - 30; var auto = $('.ui-autocomplete'); var curHeight = $(auto).children().length * 21; if (curHeight > maxHeight) { $(auto).css({maxHeight: maxHeight+'px', overflow:'scroll', zIndex: 12}); } else { $(auto).css({maxHeight: 'none', overflow:'hidden', zIndex: 12}); } } }, autoCompleteSelect: function(event, ui) { // This is a callback for autocomplete to let us know that the user selected // a gene from the list. // See http://api.jqueryui.com/autocomplete/#event-select var geneTrack = this.props.positionInfo.get('geneSuggestTrack'); this.setState({position: ui.item.id, //#*** TODO: This currently does nothing. Hook it up to the model. // highlight genes choosen from suggest list (#6330) hgFindParams: { 'track': geneTrack, 'vis': 'pack', 'extraSel': '', 'matches': ui.item.internalId } }); this.props.update(this.props.path.concat('position'), ui.item.id); // Don't let autocomplete whack the input's value: event.preventDefault(); }, componentDidMount: function() { // If we have a geneSuggest track, set up autocomplete. var inputNode, $input; inputNode = this.refs.input.getDOMNode(); $input = $(inputNode); $input.autocomplete({ delay: 500, minLength: 2, source: this.autoCompleteSourceFactory(), open: this.autoCompleteMenuOpen, select: this.autoCompleteSelect }); // IE8 voodoo... I tried logging all events on input node and disabling // ones that looked weird until I found something that prevented buggy behavior. // Without the following, if you click on an autocomplete item in IE8, then // IE8 only acts on the blur on the position field (causing an hgFind request) // and totally loses the select action (jquery-ui's autocomplete never gets // the select event). This prevents the premature blur action when we click // on an autocomplete item, as long as jquery-ui className doesn't change... if (inputNode.onbeforedeactivate !== undefined) { console.log('IE8 onbeforedeactivate hack'); $input.on('beforedeactivate', function(ev) { if (ev.originalEvent && ev.originalEvent.toElement && /ui-state-focus/.test(ev.originalEvent.toElement.className)) { ev.preventDefault(); } }); } }, render: function() { var posInfo = this.props.positionInfo; var spinner = null, posPopup = null; var loading = posInfo.get('loading'); if (loading) { spinner = <Icon type="spinner" className="floatRight" />; } var matches = posInfo.get('positionMatches'); if (matches) { posPopup = <PositionPopup positionMatches={matches} update={this.props.update} path={this.props.path}/>; } if (loading || matches) { // If the autocomplete menu is displayed when position search begins or when // position search results arrive, make the menu go away: $(this.refs.input.getDOMNode()).blur(); } return ( <div className={this.props.className}> <TextInput value={posInfo.get('position')} path={this.props.path.concat('position')} update={this.props.update} + placeholder="" size={45} ref='input' /> {spinner} {posPopup} </div> ); } }); // PositionSearch PositionPopup = React.createClass({ // Helper component: when there are multiple matches from position/search, // display them in a popup box with links for the user to choose a position. mixins: [PathUpdate, ImmutableUpdate], // update(path + 'hidePosPopup') called when user clicks to hide popup // update(path + 'positionMatch', matches): user clicks position link in popup // (matches obj is from hgFind) propTypes: { positionMatches: pt.object.isRequired, // Immutable.Map: multiple search results }, makePosMatchLink: function(matches, i) { // Display a search result; if for a specific position, make a URL to choose that pos. var position = matches.get('position'); var description = matches.get('description'); description = description ? ' - ' + description : null; var posName = matches.get('posName'); var posLabel = posName; if (position) { // Wrap posName with a link to select this position var update = this.props.update; var path = this.props.path.concat('positionMatch'); var onClick = function(e) { update(path, matches); e.preventDefault(); e.stopPropagation(); }; posLabel = <span> <a href="#" onClick={onClick} className="posLink">{posName}</a>{' at ' + position} </span>; } return ( <div key={i}> {posLabel} {description} </div> ); }, makePosPopupSection: function(trackMatches, i) { // Make a section for results from one track. return ( <div key={i}> <h3>{trackMatches.get('description')}</h3> {trackMatches.get('matches').map(this.makePosMatchLink).toJS()} <br /> </div> ); }, render: function() { // Display position matches if necessary. return ( <Modal title='Your search turned up multiple results; please choose one.' path={this.props.path.concat('hidePosPopup')} update={this.props.update}> {this.props.positionMatches.map(this.makePosPopupSection).toJS()} </Modal> ); } }); // PositionPopup // Without this, jshint complains that PositionSearch is not used. Module system would help. PositionSearch = PositionSearch;