UNPKG

@gmod/jbrowse

Version:

JBrowse - client-side genome browser

329 lines (279 loc) 11.6 kB
define(['dojo/_base/declare', 'dojo/_base/array', 'dojo/_base/lang', 'dojo/dom-construct', 'dojo/query', 'dojo/on', 'dojo/json', 'dijit/TitlePane', 'dijit/layout/ContentPane', 'JBrowse/Util', './_TextFilterMixin' ], function( declare, array, lang, dom, query, on, JSON, TitlePane, ContentPane, Util, _TextFilterMixin ) { return declare( 'JBrowse.View.TrackList.Hierarchical', [ ContentPane, _TextFilterMixin ], { region: 'left', splitter: true, style: 'width: 25%', id: 'hierarchicalTrackPane', baseClass: 'jbrowseHierarchicalTrackSelector', categoryFacet: 'category', constructor: function( args ) { this.categories = {}; this.config= lang.mixin({ "sortHierarchical": true }, args); this._loadState(); }, postCreate: function() { this.placeAt( this.browser.container ); // subscribe to commands coming from the the controller this.browser.subscribe( '/jbrowse/v1/c/tracks/show', lang.hitch( this, 'setTracksActive' )); this.browser.subscribe( '/jbrowse/v1/c/tracks/hide', lang.hitch( this, 'setTracksInactive' )); this.browser.subscribe( '/jbrowse/v1/c/tracks/new', lang.hitch( this, 'addTracks' )); this.browser.subscribe( '/jbrowse/v1/c/tracks/replace', lang.hitch( this, 'replaceTracks' )); this.browser.subscribe( '/jbrowse/v1/c/tracks/delete', lang.hitch( this, 'deleteTracks' )); }, buildRendering: function() { this.inherited('buildRendering',arguments); var topPane = new ContentPane({ className: 'header' }); this.addChild( topPane ); dom.create( 'h2', { className: 'title', innerHTML: 'Available Tracks' }, topPane.containerNode ); this._makeTextFilterNodes( dom.create('div', { className: 'textfilterContainer' }, topPane.containerNode ) ); this._updateTextFilterControl(); }, startup: function() { this.inherited('startup', arguments ); var tracks = []; var thisB = this; var categoryFacet = this.get('categoryFacet'); var sorter; if(this.config.sortHierarchical) { sorter=[ { attribute: categoryFacet.toLowerCase()}, { attribute: 'key' }, { attribute: 'label' } ]; } // add initally collapsed categories to the local storage var arr=(this.get('collapsedCategories')||"").split(","); for(var i=0; i<arr.length;i++) { lang.setObject('collapsed.'+arr[i],true,this.state); } this._saveState(); this.get('trackMetaData').fetch( { onItem: function(i) { if( i.conf ) tracks.push( i ); }, onComplete: function() { // make a pane at the top to hold uncategorized tracks thisB.categories.Uncategorized = { pane: new ContentPane({ className: 'uncategorized' }).placeAt( thisB.containerNode ), tracks: {}, categories: {} }; thisB.addTracks( tracks, true ); // hide the uncategorized pane if it is empty if( ! thisB.categories.Uncategorized.pane.containerNode.children.length ) { //thisB.removeChild( thisB.categories.Uncategorized.pane ); thisB.categories.Uncategorized.pane.domNode.style.display = 'none'; } }, sort: sorter }); }, addTracks: function( tracks, inStartup ) { this.pane = this; var thisB = this; array.forEach( tracks, function( track ) { var trackConf = track.conf || track; var categoryFacet = this.get('categoryFacet'); var categoryNames = ( trackConf.metadata && trackConf.metadata[ categoryFacet ] || trackConf[ categoryFacet ] || track[ categoryFacet ] || 'Uncategorized' ).split(/\s*\/\s*/); var category = _findCategory( this, categoryNames, [] ); function _findCategory( obj, names, path ) { var categoryName = names.shift(); path = path.concat(categoryName); var categoryPath = path.join('/'); var cat = obj.categories[categoryName] || ( obj.categories[categoryName] = function() { var isCollapsed = lang.getObject( 'collapsed.'+categoryPath, false, thisB.state ); var c = new TitlePane( { title: '<span class="categoryName">'+categoryName+'</span>' + ' <span class="trackCount">0</span>', open: ! isCollapsed }); // save our open/collapsed state in local storage c.watch( 'open', function( attr, oldval, newval ) { lang.setObject( 'collapsed.'+categoryPath, !newval, thisB.state ); thisB._saveState(); }); obj.pane.addChild(c, inStartup ? undefined : 1 ); return { parent: obj, pane: c, categories: {}, tracks: {} }; }.call(thisB)); return names.length ? _findCategory( cat, names, path ) : cat; }; category.pane.domNode.style.display = 'block'; // note: sometimes trackConf.description is defined as numeric, so in this case, ignore it var labelNode = dom.create( 'label', { className: 'tracklist-label shown', title: Util.escapeHTML( trackConf.shortDescription || track.shortDescription || (trackConf.description===1?undefined:trackConf.description) || track.description || trackConf.Description || track.Description || trackConf.metadata && ( trackConf.metadata.shortDescription || trackConf.metadata.description || trackConf.metadata.Description ) || track.key || trackConf.key || trackConf.label ) }, category.pane.containerNode ); var checkBoxProps = { type: 'checkbox', className: 'check' }; // hook point if (typeof thisB.extendCheckbox === 'function') var checkBoxProps = thisB.extendCheckbox(checkBoxProps,trackConf); var checkbox = dom.create('input', checkBoxProps, labelNode ); var trackLabel = trackConf.label; var checkListener; this.own( checkListener = on( checkbox, 'click', function() { thisB.itemClick(this,trackConf); })); dom.create('span', { className: 'key', innerHTML: trackConf.key || trackConf.label }, labelNode ); category.tracks[ trackLabel ] = { checkbox: checkbox, checkListener: checkListener, labelNode: labelNode }; this._updateTitles( category ); }, this ); }, // called when item checkbox is clicked. itemClick: function(checkbox,trackConf) { this.browser.publish( '/jbrowse/v1/v/tracks/'+(checkbox.checked ? 'show' : 'hide'), [trackConf] ); }, _loadState: function() { this.state = {}; try { this.state = JSON.parse( localStorage.getItem( 'JBrowse-Hierarchical-Track-Selector' ) || '{}' ); } catch(e) {} return this.state; }, _saveState: function( state ) { try { localStorage.setItem( 'JBrowse-Hierarchical-Track-Selector', JSON.stringify( this.state ) ); } catch(e) {} }, // depth-first traverse and update the titles of all the categories _updateAllTitles: function(r) { var root = r || this; for( var c in root.categories ) { this._updateTitle( root.categories[c] ); this._updateAllTitles( root.categories[c] ); } }, _updateTitle: function( category ) { category.pane.set( 'title', category.pane.get('title') .replace( />\s*\d+\s*</, '>'+query('label.shown', category.pane.containerNode ).length+'<' ) ); }, // update the titles of the given category and its parents _updateTitles: function( category ) { this._updateTitle( category ); if( category.parent ) this._updateTitles( category.parent ); }, _findTrack: function _findTrack( trackLabel, callback, r ) { var root = r || this; for( var c in root.categories ) { var category = root.categories[c]; if( category.tracks[ trackLabel ] ) { callback( category.tracks[ trackLabel ], category ); return true; } else { if( this._findTrack( trackLabel, callback, category ) ) return true; } } return false; }, // hook point replaceTracks: function( trackConfigs ) { // notification }, /** * Given an array of track configs, update the track list to show * that they are turned on. */ setTracksActive: function( /**Array[Object]*/ trackConfigs ) { array.forEach( trackConfigs, function(conf) { this._findTrack( conf.label, function( trackRecord, category ) { trackRecord.checkbox.checked = true; }); },this); }, deleteTracks: function( /**Array[Object]*/ trackConfigs ) { array.forEach( trackConfigs, function(conf) { this._findTrack( conf.label, function( trackRecord, category ) { trackRecord.labelNode.parentNode.removeChild( trackRecord.labelNode ); trackRecord.checkListener.remove(); delete category.tracks[conf.label]; }); },this); }, /** * Given an array of track configs, update the track list to show * that they are turned off. */ setTracksInactive: function( /**Array[Object]*/ trackConfigs ) { array.forEach( trackConfigs, function(conf) { this._findTrack( conf.label, function( trackRecord, category ) { trackRecord.checkbox.checked = false; }); },this); }, _textFilter: function() { this.inherited(arguments); this._updateAllTitles(); }, /** * Make the track selector visible. * This does nothing for this track selector, since it is always visible. */ show: function() { }, /** * Make the track selector invisible. * This does nothing for this track selector, since it is always visible. */ hide: function() { }, /** * Toggle visibility of this track selector. * This does nothing for this track selector, since it is always visible. */ toggle: function() { } }); });