@gmod/jbrowse
Version:
JBrowse - client-side genome browser
340 lines (295 loc) • 12.2 kB
JavaScript
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( args ) {
this.categories = {};
this.config=
lang.mixin({
"sortHierarchical": true
},
args);
this._loadState();
},
postCreate() {
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() {
this.inherited('buildRendering',arguments);
var topPane = dom.create('div', { className: 'header' }, this.containerNode);
dom.create(
'h2',
{ className: 'title',
innerHTML: 'Available Tracks'
},
topPane );
this._makeTextFilterNodes(
dom.create('div',
{ className: 'textfilterContainer' },
topPane )
);
this._updateTextFilterControl();
},
induceCategoryOrder(tracks, categoryOrder) {
const order = categoryOrder.split(",").map(s => s.trim()).map(s => s.split("/").map(s => s.trim()).join('/'))
tracks.forEach(t => {
if(t.category) {
t.cat = t.category.trim().split('/').map(s=>s.trim()).join('/')
}
})
var unordered = tracks.filter(t => order.indexOf(t.cat) === -1);
var ordered = tracks.filter(t => order.indexOf(t.cat) !== -1);
ordered.sort((a, b) => {
return order.indexOf(a.cat) - order.indexOf(b.cat);
});
tracks.forEach(t => delete t.cat)
return ordered.concat(unordered)
},
startup() {
this.inherited('startup', arguments );
var tracks = [];
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(",").map(s => s.trim()).map(s => s.split("/").map(s => s.trim()).join('/'));
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: () => {
// make a pane at the top to hold uncategorized tracks
this.categories.Uncategorized = {
pane: new ContentPane({ className: 'uncategorized' }).placeAt( this.containerNode ),
tracks: {},
categories: {}
};
if( this.config.categoryOrder ) {
tracks = this.induceCategoryOrder(tracks, this.config.categoryOrder)
}
this.addTracks( tracks, true );
// hide the uncategorized pane if it is empty
if( ! this.categories.Uncategorized.pane.containerNode.children.length ) {
this.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 );
this._updateAllTitles()
},
// 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() {
}
});
});