@gmod/jbrowse
Version:
JBrowse - client-side genome browser
369 lines (319 loc) • 12.7 kB
JavaScript
define(['dojo/_base/declare',
'dojo/_base/array',
'dojo/_base/event',
'dojo/keys',
'dojo/on',
'dojo/dom-construct',
'dojo/dom-class',
'dijit/layout/ContentPane',
'dojo/dnd/Source',
'dojo/fx/easing',
'dijit/form/TextBox',
'./_TextFilterMixin'
],
function(
declare,
array,
event,
keys,
on,
dom,
domClass,
ContentPane,
dndSource,
animationEasing,
dijitTextBox,
_TextFilterMixin
) {
return declare( 'JBrowse.View.TrackList.Simple', _TextFilterMixin,
/** @lends JBrowse.View.TrackList.Simple.prototype */
{
/**
* Simple drag-and-drop track selector.
* @constructs
*/
constructor: function( args ) {
this.browser = args.browser;
// make the track list DOM nodes and widgets
this.createTrackList( args.browser.container );
// maintain a list of the HTML nodes of inactive tracks, so we
// can flash them and whatnot
this.inactiveTrackNodes = {};
// populate our track list (in the right order)
this.trackListWidget.insertNodes(
false,
args.trackConfigs
);
// subscribe to drop events for tracks being DND'ed
this.browser.subscribe(
"/dnd/drop",
dojo.hitch( this,
function( source, nodes, copy, target ){
if( target !== this.trackListWidget )
return;
// get the configs from the tracks being dragged in
var confs = dojo.filter(
dojo.map( nodes, function(n) {
return n.track && n.track.config;
}
),
function(c) {return c;}
);
// return if no confs; whatever was
// dragged here probably wasn't a
// track
if( ! confs.length )
return;
this.dndDrop = true;
this.browser.publish( '/jbrowse/v1/v/tracks/hide', confs );
this.dndDrop = false;
}
));
// subscribe to commands coming from the the controller
this.browser.subscribe( '/jbrowse/v1/c/tracks/show',
dojo.hitch( this, 'setTracksActive' ));
this.browser.subscribe( '/jbrowse/v1/c/tracks/hide',
dojo.hitch( this, 'setTracksInactive' ));
this.browser.subscribe( '/jbrowse/v1/c/tracks/new',
dojo.hitch( this, 'addTracks' ));
this.browser.subscribe( '/jbrowse/v1/c/tracks/replace',
dojo.hitch( this, 'replaceTracks' ));
this.browser.subscribe( '/jbrowse/v1/c/tracks/delete',
dojo.hitch( this, 'deleteTracks' ));
},
addTracks: function( trackConfigs ) {
// note that new tracks are, by default, hidden, so we just put them in the list
this.trackListWidget.insertNodes(
false,
trackConfigs
);
this._blinkTracks( trackConfigs );
},
replaceTracks: function( trackConfigs ) {
// for each one
array.forEach( trackConfigs, function( conf ) {
var oldNode = this.inactiveTrackNodes[ conf.label ];
if( ! oldNode )
return;
delete this.inactiveTrackNodes[ conf.label ];
this.trackListWidget.delItem( oldNode.id );
if( oldNode.parentNode )
oldNode.parentNode.removeChild( oldNode );
this.trackListWidget.insertNodes( false, [conf], false, oldNode.previousSibling );
},this);
},
/** @private */
createTrackList: function( renderTo ) {
var leftPane = dojo.create(
'div',
{ id: 'trackPane',
className: 'jbrowseSimpleTrackSelector',
style: { width: '12em' }
},
renderTo
);
//splitter on left side
var leftWidget = new ContentPane({region: "left", splitter: true}, leftPane);
var trackListDiv = this.div = this.containerNode = dojo.create(
'div',
{ id: 'tracksAvail',
className: 'container handles',
style: { width: '100%', height: '100%', overflowX: 'hidden', overflowY: 'auto' },
innerHTML: '<h2>Available Tracks</h2>'
},
leftPane
);
this._makeTextFilterNodes( trackListDiv );
this._updateTextFilterControl();
this.trackListWidget = new dndSource(
trackListDiv,
{
accept: ["track"], // accepts only tracks into left div
withHandles: false,
creator: dojo.hitch( this, function( trackConfig, hint ) {
var key = trackConfig.key || trackConfig.name || trackConfig.label;
var node = dojo.create(
'div',
{ className: 'tracklist-label',
title: key+' (drag or double-click to activate)',
innerHTML: key
}
);
//in the list, wrap the list item in a container for
//border drag-insertion-point monkeying
if ("avatar" != hint) {
on(node, "dblclick", dojo.hitch(this, function() {
this.browser.publish( '/jbrowse/v1/v/tracks/show', [trackConfig] );
}));
var container = dojo.create( 'div', { className: 'tracklist-container' });
container.appendChild(node);
node = container;
node.id = dojo.dnd.getUniqueId();
this.inactiveTrackNodes[trackConfig.label] = node;
}
return {node: node, data: trackConfig, type: ["track"]};
})
}
);
// The dojo onMouseDown and onMouseUp methods don't support the functionality we're looking for,
// so we'll substitute our own
this.trackListWidget.onMouseDown = dojo.hitch(this, "onMouseDown");
this.trackListWidget.onMouseUp = dojo.hitch(this, "onMouseUp");
// We want the escape key to deselect all tracks
on(document, "keydown", dojo.hitch(this, "onKeyDown"));
return trackListDiv;
},
onKeyDown: function(e) {
switch(e.keyCode) {
case keys.ESCAPE:
this.trackListWidget.selectNone();
break;
}
},
onMouseDown: function(e) {
var thisW = this.trackListWidget;
if(!thisW.mouseDown && thisW._legalMouseDown(e)){
thisW.mouseDown = true;
thisW._lastX = e.pageX;
thisW._lastY = e.pageY;
this._onMouseDown(thisW.current, e);
}
},
_onMouseDown: function(current, e) {
if(!current) return;
var thisW = this.trackListWidget;
if(!e.ctrlKey && !e.shiftKey) {
thisW.simpleSelection = true;
if(!this._isSelected(current)) {
thisW.selectNone();
thisW.simpleSelection = false;
}
}
if(e.shiftKey && this.anchor) {
var i = 0;
var nodes = thisW.getAllNodes();
this._select(current);
if(current != this.anchor) {
for(; i < nodes.length; i++) {
if(nodes[i] == this.anchor || nodes[i] == current) break;
}
i++;
for(; i < nodes.length; i++) {
if(nodes[i] == this.anchor || nodes[i] == current) break;
this._select(nodes[i]);
}
}
} else {
e.ctrlKey ? this._toggle(current) : this._select(current);
this.anchor = current;
}
event.stop(e);
},
onMouseUp: function(e) {
var thisW = this.trackListWidget;
if(thisW.mouseDown){
thisW.mouseDown = false;
this._onMouseUp(e);
}
},
_onMouseUp: function(e) {
var thisW = this.trackListWidget;
if(thisW.simpleSelection && thisW.current) {
thisW.selectNone();
this._select(thisW.current);
}
},
_isSelected: function(node) {
return this.trackListWidget.selection[node.id];
},
_select: function(node) {
this.trackListWidget.selection[node.id] = 1;
this.trackListWidget._addItemClass(node, "Selected");
},
_deselect: function(node) {
delete this.trackListWidget.selection[node.id];
this.trackListWidget._removeItemClass(node, "Selected");
},
_toggle: function(node) {
if(this.trackListWidget.selection[node.id]) {
this._deselect(node);
} else {
this._select(node);
}
},
/**
* Given an array of track configs, update the track list to show
* that they are turned on. For this list, that just means
* deleting them from our widget.
*/
setTracksActive: function( /**Array[Object]*/ trackConfigs ) {
this.deleteTracks( trackConfigs );
},
deleteTracks: function( /**Array[Object]*/ trackConfigs ) {
// remove any tracks in our track list that are being set as visible
array.forEach( trackConfigs || [], function( conf ) {
var oldNode = this.inactiveTrackNodes[ conf.label ];
if( ! oldNode )
return;
delete this.inactiveTrackNodes[ conf.label ];
if( oldNode.parentNode )
oldNode.parentNode.removeChild( oldNode );
this.trackListWidget.delItem( oldNode.id );
},this);
},
/**
* Given an array of track configs, update the track list to show
* that they are turned off.
*/
setTracksInactive: function( /**Array[Object]*/ trackConfigs ) {
// remove any tracks in our track list that are being set as visible
if( ! this.dndDrop ) {
var n = this.trackListWidget.insertNodes( false, trackConfigs );
// blink the track(s) that we just turned off to make it
// easier for users to tell where they went.
// note that insertNodes will have put its html element in
// inactivetracknodes
this._blinkTracks( trackConfigs );
}
},
_blinkTracks: function( trackConfigs ) {
// scroll the tracklist all the way to the bottom so we can see the blinking nodes
this.trackListWidget.node.scrollTop = this.trackListWidget.node.scrollHeight;
array.forEach( trackConfigs, function(c) {
var label = this.inactiveTrackNodes[c.label].firstChild;
if( label ) {
dojo.animateProperty({
node: label,
duration: 400,
properties: {
backgroundColor: { start: '#DEDEDE', end: '#FFDE2B' }
},
easing: animationEasing.sine,
repeat: 2,
onEnd: function() {
label.style.backgroundColor = null;
}
}).play();
}
},this);
},
/**
* Make the track selector visible.
* This does nothing for the Simple track selector, since it is always visible.
*/
show: function() {
},
/**
* Make the track selector invisible.
* This does nothing for the Simple track selector, since it is always visible.
*/
hide: function() {
},
/**
* Toggle visibility of this track selector.
* This does nothing for the Simple track selector, since it is always visible.
*/
toggle: function() {
}
});
});