@gmod/jbrowse
Version:
JBrowse - client-side genome browser
917 lines (814 loc) • 35.5 kB
JavaScript
define(
[
'dojo/_base/declare',
'dojo/_base/array',
'dojo/_base/lang',
'dojo/Deferred',
'dojo/promise/all',
'dijit/TitlePane',
'dijit/layout/ContentPane',
'JBrowse/Util',
'dojox/grid/EnhancedGrid',
'dojox/grid/enhanced/plugins/IndirectSelection'
],
function (
declare,
array,
lang,
Deferred,
all,
TitlePane,
ContentPane,
Util,
EnhancedGrid
) {
var dojof = Util.dojof;
return declare( 'JBrowse.View.TrackList.Faceted', null,
/**
* @lends JBrowse.View.TrackList.Faceted.prototype
*/
{
/**
* Track selector with facets and text searching.
* @constructs
*/
constructor: function(args) {
this.browser = args.browser;
this.tracksActive = {};
this.config = args;
this.storeReady = new Deferred()
this.gridReady = new Deferred()
this.ready = all([this.storeReady,this.gridReady])
// construct the discriminator for whether we will display a
// facet selector for this facet
this._isSelectableFacet = this._coerceFilter(
args.selectableFacetFilter
// default facet filtering function
|| function( facetName, store ){
return (
// has an avg bucket size > 1
store.getFacetStats( facetName ).avgBucketSize > 1
&&
// and not an ident or label attribute
! array.some( store.getLabelAttributes()
.concat( store.getIdentityAttributes() ),
function(l) {return l == facetName;}
)
);
}
);
// construct a similar discriminator for which columns will be displayed
this.displayColumns = args.displayColumns;
this._isDisplayableColumn = this._coerceFilter(
args.displayColumnFilter || function(l) { return l.toLowerCase() != 'label'; }
);
// data store that fetches and filters our track metadata
this.trackDataStore = args.trackMetaData;
// subscribe to commands coming from the the controller
this.browser.subscribe( '/jbrowse/v1/c/tracks/show',
lang.hitch( this, 'setTracksActive' ));
// subscribe to commands coming from the the controller
this.browser.subscribe( '/jbrowse/v1/c/tracks/hide',
lang.hitch( this, 'setTracksInactive' ));
this.browser.subscribe( '/jbrowse/v1/c/tracks/delete',
lang.hitch( this, 'setTracksInactive' ));
this.renderInitial();
// once its data is loaded and ready
this.trackDataStore.onReady( this, function() {
// render our controls and so forth
this.renderSelectors();
// connect events so that when a grid row is selected or
// deselected (with the checkbox), publish a message
// indicating that the user wants that track turned on or
// off
dojo.connect( this.dataGrid.selection, 'onSelected', this, function(index) {
this._ifNotSuppressed( 'selectionEvents', function() {
this._suppress( 'gridUpdate', function() {
this.browser.publish( '/jbrowse/v1/v/tracks/show', [this.dataGrid.getItem( index ).conf] );
});
});
});
dojo.connect( this.dataGrid.selection, 'onDeselected', this, function(index) {
this._ifNotSuppressed( 'selectionEvents', function() {
this._suppress( 'gridUpdate', function() {
this.browser.publish( '/jbrowse/v1/v/tracks/hide', [this.dataGrid.getItem( index ).conf] );
});
});
});
this._updateFacetCounts()
this._updateMatchCount()
this.storeReady.resolve()
dojo.connect( this.trackDataStore, 'onFetchSuccess', this, () => {
this._updateGridSelections()
this._updateMatchCount()
});
});
},
/**
* Coerces a string or array of strings into a function that,
* given a string, returns true if the string matches one of the
* given strings. If passed a function, just returns that
* function.
* @private
*/
_coerceFilter: function( filter ) {
// if we have a non-function filter, coerce to an array,
// then convert that array to a function
if( typeof filter == 'string' )
filter = [filter];
if( dojo.isArray( filter ) ) {
filter = function( store, facetName) {
return array.some( filter, function(fn) {
return facetName == fn;
});
};
}
return filter;
},
/**
* Call the given callback if none of the given event suppression flags are set.
* @private
*/
_ifNotSuppressed: function( suppressFlags, callback ) {
if( typeof suppressFlags == 'string')
suppressFlags = [suppressFlags];
if( !this.suppress)
this.suppress = {};
if( array.some( suppressFlags, function(f) {return this.suppress[f];}, this) )
return undefined;
return callback.call(this);
},
/**
* Call the given callback while setting the given event suppression flags.
* @private
*/
_suppress: function( suppressFlags, callback ) {
if( typeof suppressFlags == 'string')
suppressFlags = [suppressFlags];
if( !this.suppress)
this.suppress = {};
dojo.forEach( suppressFlags, function(f) {this.suppress[f] = true; }, this);
var retval = callback.call( this );
dojo.forEach( suppressFlags, function(f) {this.suppress[f] = false;}, this);
return retval;
},
_suppressAsync: function( suppressFlags, callback ) {
if( typeof suppressFlags == 'string')
suppressFlags = [suppressFlags];
if( !this.suppress)
this.suppress = {};
dojo.forEach( suppressFlags, function(f) {this.suppress[f] = true; }, this);
return callback.call( this )
.then(
retval => {
suppressFlags.forEach( f => this.suppress[f] = false )
return retval;
},
err => {
suppressFlags.forEach( f => this.suppress[f] = false )
console.error(err)
}
)
},
/**
* Call a method of our object such that it cannot call itself
* by way of event cycles.
* @private
*/
_suppressRecursion: function( methodName ) {
var flag = ['method_'+methodName];
var method = this[methodName];
return this._ifNotSuppressed( flag, function() { this._suppress( flag, method );});
},
renderInitial: function() {
this.containerElem = dojo.create( 'div', {
id: 'faceted_tracksel',
className: 'jbrowse',
style: {
left: '-95%',
width: '95%',
zIndex: 500
}
},
document.body );
// make the tab that turns the selector on and off
dojo.create('div',
{
className: 'faceted_tracksel_on_off tab',
innerHTML: '<img src="'+this.browser.resolveUrl('img/left_arrow.png')+'"><div>Select<br>tracks</div>'
},
this.containerElem
);
this.mainContainer = new dijit.layout.BorderContainer(
{ design: 'headline', gutters: false },
dojo.create('div',{ className: 'mainContainer' }, this.containerElem)
);
this.topPane = new dijit.layout.ContentPane(
{ region: 'top',
id: "faceted_tracksel_top",
content: '<div class="title">Select Tracks</div> '
+ '<div class="topLink" style="cursor: help"><a title="Track selector help">Help</a></div>'
});
dojo.query('div.topLink a[title="Track selector help"]',this.topPane.domNode)
.forEach(function(helplink){
var helpdialog = new dijit.Dialog({
"class": 'jbrowse help_dialog',
refocus: false,
draggable: false,
title: 'Track Selection',
content: '<div class="main">'
+ '<p>The JBrowse Faceted Track Selector makes it easy to search through'
+ ' large numbers of available tracks to find exactly the ones you want.'
+ ' You can incrementally filter the track display to narrow it down to'
+ ' those your are interested in. There are two types of filtering available,'
+ ' which can be used together:'
+ ' <b>filtering with data fields</b>, and free-form <b>filtering with text</b>.'
+ '</p>'
+ ' <dl><dt>Filtering with Data Fields</dt>'
+ ' <dd>The left column of the display contains the available <b>data fields</b>. Click on the data field name to expand it, and then select one or more values for that field. This narrows the search to display only tracks that have one of those values for that field. You can do this for any number of fields.<dd>'
+ ' <dt>Filtering with Text</dt>'
+ ' <dd>Type text in the "Contains text" box to filter for tracks whose data contains that text. If you type multiple words, tracks are filtered such that they must contain all of those words, in any order. Placing "quotation marks" around the text filters for tracks that contain that phrase exactly. All text matching is case insensitive.</dd>'
+ ' <dt>Activating Tracks</dt>'
+ " <dd>To activate and deactivate a track, click its check-box in the left-most column. When the box contains a check mark, the track is activated. You can also turn whole groups of tracks on and off using the check-box in the table heading.</dd>"
+ " </dl>"
+ "</div>"
});
dojo.connect( helplink, 'onclick', this, function(evt) {helpdialog.show(); return false;});
},this);
this.mainContainer.addChild( this.topPane );
// make both buttons toggle this track selector
dojo.query( '.faceted_tracksel_on_off' )
.onclick( lang.hitch( this, 'toggle' ));
this.centerPane = new dijit.layout.BorderContainer({region: 'center', "class": 'gridPane', gutters: false});
this.mainContainer.addChild( this.centerPane );
var textFilterContainer = this.renderTextFilter();
this.busyIndicator = dojo.create(
'div', {
innerHTML: '<img src="'+this.browser.resolveUrl('img/spinner.gif')+'">',
className: 'busy_indicator'
}, this.containerElem );
this.centerPane.addChild(
new dijit.layout.ContentPane(
{ region: 'top',
"class": 'gridControls',
content: [
dojo.create( 'button', {
className: 'faceted_tracksel_on_off',
innerHTML: '<img src="'+this.browser.resolveUrl('img/left_arrow.png')+'"> <div>Back to browser</div>',
onclick: lang.hitch( this, 'hide' )
}
),
dojo.create( 'button', {
className: 'clear_filters',
innerHTML:'<img src="'+this.browser.resolveUrl('img/red_x.png')+'">'
+ '<div>Clear All Filters</div>',
onclick: lang.hitch( this, function(evt) {
this._clearTextFilterControl();
this._clearAllFacetControls();
this._async( function() {
this.updateQuery();
this._updateFacetCounts();
},this).call();
})
}
),
this.busyIndicator,
textFilterContainer,
dojo.create('div', { className: 'matching_record_count' })
]
}
)
);
},
renderSelectors: function() {
// make our main components
var facetContainer = this.renderFacetSelectors();
// put them in their places in the overall layout of the track selector
facetContainer.set('region','left');
this.mainContainer.addChild( facetContainer );
this.dataGrid = this.renderGrid();
this.dataGrid.set('region','center');
// code around a dijit bug with width calculation in IE.
// doesn't seem to harm other browsers, the width gets overwritten anyway
// by dijit's calculations.
this.dataGrid.domNode.style.width = '500px'
this.centerPane.addChild( this.dataGrid );
this.mainContainer.startup();
this.gridReady.resolve()
},
/** do something in a timeout to avoid blocking the UI */
_async: function( func, scope ) {
var that = this;
return function() {
var args = arguments;
var nativeScope = this;
that._busy( true );
window.setTimeout(
function() {
func.apply( scope || nativeScope, args );
that._busy( false );
},
50
);
};
},
_busy: function( busy ) {
this.busyCount = Math.max( 0, (this.busyCount || 0) + ( busy ? 1 : -1 ) );
if( this.busyCount > 0 )
dojo.addClass( this.containerElem, 'busy' );
else
dojo.removeClass( this.containerElem, 'busy' );
},
renderGrid: function() {
var displayColumns = this.displayColumns
|| dojo.filter(
this.trackDataStore.getFacetNames(),
lang.hitch(this, '_isDisplayableColumn')
);
var colWidth = 90/displayColumns.length;
var grid = new EnhancedGrid({
id: 'trackSelectGrid',
store: this.trackDataStore,
selectable: true,
escapeHTMLInData: ('escapeHTMLInData' in this.config) ? this.config.escapeHTMLInData : false,
noDataMessage: "No tracks match the filtering criteria.",
structure: [
dojo.map(
displayColumns,
function(facetName) {
// rename name to key to avoid configuration confusion
facetName = {name: 'key'}[facetName.toLowerCase()] || facetName;
return {'name': this._facetDisplayName(facetName), 'field': facetName.toLowerCase(), 'width': colWidth+'%'};
},
this
)
],
plugins: {
indirectSelection: {
headerSelector: true
}
}
}
);
// set the grid's initial sort index
var sortIndex = this.config.initialSortColumn || 0;
if( typeof sortIndex == 'string' )
sortIndex = array.indexOf( displayColumns, sortIndex );
grid.setSortIndex( sortIndex+1 );
// monkey-patch the grid to customize some of its behaviors
this._monkeyPatchGrid( grid );
return grid;
},
/**
* Given a raw facet name, format it for user-facing display.
* @private
*/
_facetDisplayName: function( facetName ) {
// make renameFacets if needed, and lowercase all the keys to
// make it case-insensitive
this.renameFacets = this.renameFacets || function(){
var renameFacets = this.config.renameFacets;
var lc = {};
for( var k in renameFacets ) {
lc[ k.toLowerCase() ] = renameFacets[k];
}
lc.key = lc.key || 'Name';
return lc;
}.call(this);
return this.renameFacets[facetName.toLowerCase()] || Util.ucFirst( facetName.replace('_',' ') );
},
/**
* Apply several run-time patches to the dojox.grid.EnhancedGrid
* code to fix bugs and customize the behavior in ways that aren't
* quite possible using the regular Dojo APIs.
* @private
*/
_monkeyPatchGrid: function( grid ) {
// 1. monkey-patch the grid's onRowClick handler to not do
// anything. without this, clicking on a row selects it, and
// deselects everything else, which is quite undesirable.
grid.onRowClick = function() {};
// 2. monkey-patch the grid's range-selector to refuse to select
// if the selection is too big
var origSelectRange = grid.selection.selectRange;
grid.selection.selectRange = function( inFrom, inTo ) {
var selectionLimit = 30;
if( inTo - inFrom > selectionLimit ) {
alert( "Too many tracks selected, please select fewer than "+selectionLimit+" tracks. Note: you can use shift+click to select a range of tracks" );
return undefined;
}
return origSelectRange.apply( this, arguments );
};
},
renderTextFilter: function( parent ) {
// make the text input for text filtering
this.textFilterLabel = dojo.create(
'label',
{ className: 'textFilterControl',
innerHTML: 'Contains text ',
id: 'tracklist_textfilter',
style: {position: 'relative'}
},
parent
);
this.textFilterInput = dojo.create(
'input',
{ type: 'text',
size: 40,
disabled: true, // disabled until shown
onkeypress: lang.hitch( this, function(evt) {
// don't pay attention to modifier keys
if( evt.keyCode == dojo.keys.SHIFT || evt.keyCode == dojo.keys.CTRL || evt.keyCode == dojo.keys.ALT )
return;
// use a timeout to avoid updating the display too fast
if( this.textFilterTimeout )
window.clearTimeout( this.textFilterTimeout );
this.textFilterTimeout = window.setTimeout(
lang.hitch( this, function() {
// do a new search and update the display
this._updateTextFilterControl();
this._async( function() {
this.updateQuery();
this._updateFacetCounts();
this.textFilterInput.focus();
},this).call();
this.textFilterInput.focus();
}),
500
);
this._updateTextFilterControl();
evt.stopPropagation();
})
},
this.textFilterLabel
);
// make a "clear" button for the text filtering input
this.textFilterClearButton = dojo.create('img', {
src: this.browser.resolveUrl('img/red_x.png'),
className: 'text_filter_clear',
onclick: lang.hitch( this, function() {
this._clearTextFilterControl();
this._async( function() {
this.updateQuery();
this._updateFacetCounts();
},this).call();
}),
style: {
position: 'absolute',
right: '4px',
top: '20%'
}
}, this.textFilterLabel );
return this.textFilterLabel;
},
/**
* Clear the text filter control input.
* @private
*/
_clearTextFilterControl: function() {
this.textFilterInput.value = '';
this._updateTextFilterControl();
},
/**
* Update the display of the text filter control based on whether
* it has any text in it.
* @private
*/
_updateTextFilterControl: function() {
if( this.textFilterInput.value.length )
dojo.addClass( this.textFilterLabel, 'selected' );
else
dojo.removeClass( this.textFilterLabel, 'selected' );
},
/**
* Create selection boxes for each searchable facet.
*/
renderFacetSelectors: function() {
var container = new ContentPane({style: 'width: 200px'});
var store = this.trackDataStore;
this.facetSelectors = {};
// render a facet selector for a pseudo-facet holding
// attributes regarding the tracks the user has been working
// with
var usageFacet = this._renderFacetSelector(
'My Tracks', ['Currently Active', 'Recently Used'] );
usageFacet.set('class', 'myTracks' );
container.addChild( usageFacet );
// for the facets from the store, only render facet selectors
// for ones that are not identity attributes, and have an
// average bucket size greater than 1
var selectableFacets =
dojo.filter( this.config.selectableFacets || store.getFacetNames(),
function( facetName ) {
return this._isSelectableFacet( facetName, this.trackDataStore );
},
this
);
dojo.forEach( selectableFacets, function(facetName) {
// get the values of this facet
var values = store.getFacetValues(facetName).sort();
if( !values || !values.length )
return;
var facetPane = this._renderFacetSelector( facetName, values );
container.addChild( facetPane );
},this);
return container;
},
/**
* Make HTML elements for a single facet selector.
* @private
* @returns {dijit.layout.TitlePane}
*/
_renderFacetSelector: function( /**String*/ facetName, /**Array[String]*/ values ) {
var facetPane = new TitlePane(
{
title: '<span id="facet_title_' + facetName +'" '
+ 'class="facetTitle">'
+ this._facetDisplayName(facetName)
+ ' <a class="clearFacet"><img src="'+this.browser.resolveUrl('img/red_x.png')+'" /></a>'
+ '</span>'
});
// make a selection control for the values of this facet
var facetControl = dojo.create( 'table', {className: 'facetSelect'}, facetPane.containerNode );
// populate selector's options
this.facetSelectors[facetName] = dojo.map(
values,
function(val) {
var that = this;
var node = dojo.create(
'tr',
{ className: 'facetValue',
innerHTML: '<td class="count"></td><td class="value">'+ val + '</td>',
onclick: function(evt) {
dojo.toggleClass(this, 'selected');
that._updateFacetControl( facetName );
that._async( function() {
that.updateQuery();
that._updateFacetCounts( facetName );
}).call();
}
},
facetControl
);
node.facetValue = val;
return node;
},
this
);
return facetPane;
},
/**
* Clear all the selections from all of the facet controls.
* @private
*/
_clearAllFacetControls: function() {
dojo.forEach( dojof.keys( this.facetSelectors ), function( facetName ) {
this._clearFacetControl( facetName );
},this);
},
/**
* Clear all the selections from the facet control with the given name.
* @private
*/
_clearFacetControl: function( facetName ) {
dojo.forEach( this.facetSelectors[facetName] || [], function(selector) {
dojo.removeClass(selector,'selected');
},this);
this._updateFacetControl( facetName );
},
/**
* Incrementally update the facet counts as facet values are selected.
* @private
*/
_updateFacetCounts: function( /**String*/ skipFacetName ) {
dojo.forEach( dojof.keys( this.facetSelectors ), function( facetName ) {
if( facetName == 'My Tracks' )// || facetName == skipFacetName )
return;
var thisFacetCounts = this.trackDataStore.getFacetCounts( facetName );
dojo.forEach( this.facetSelectors[facetName] || [], function( selectorNode ) {
dojo.query('.count',selectorNode)
.forEach( function(countNode) {
var count = thisFacetCounts ? thisFacetCounts[ selectorNode.facetValue ] || 0 : 0;
countNode.innerHTML = Util.addCommas( count );
if( count )
dojo.removeClass( selectorNode, 'disabled');
else
dojo.addClass( selectorNode, 'disabled' );
},this);
//dojo.removeClass(selector,'selected');
},this);
this._updateFacetControl( facetName );
},this);
},
/**
* Update the title bar of the given facet control to reflect
* whether it has selected values in it.
*/
_updateFacetControl: function( facetName ) {
var titleContent = dojo.byId('facet_title_'+facetName);
// if all our values are disabled, add 'disabled' to our
// title's CSS classes
if( array.every( this.facetSelectors[facetName] ||[], function(sel) {
return dojo.hasClass( sel, 'disabled' );
},this)
) {
dojo.addClass( titleContent, 'disabled' );
}
// if we have some selected values, make a "clear" button, and
// add 'selected' to our title's CSS classes
if( array.some( this.facetSelectors[facetName] || [], function(sel) {
return dojo.hasClass( sel, 'selected' );
}, this ) ) {
var clearFunc = lang.hitch( this, function(evt) {
this._clearFacetControl( facetName );
this._async( function() {
this.updateQuery();
this._updateFacetCounts( facetName );
},this).call();
evt.stopPropagation();
});
dojo.addClass( titleContent.parentNode.parentNode, 'activeFacet' );
dojo.query( '> a', titleContent )
.forEach(function(node) { node.onclick = clearFunc; },this)
.attr('title','clear selections');
}
// otherwise, no selected values
else {
dojo.removeClass( titleContent.parentNode.parentNode, 'activeFacet' );
dojo.query( '> a', titleContent )
.onclick( function(){return false;})
.removeAttr('title');
}
},
/**
* Update the query we are using with the track metadata store
* based on the values of the search form elements.
*/
updateQuery: function() {
this._suppressRecursion( '_updateQuery' );
},
_updateQuery: function() {
var newQuery = {};
var is_selected = function(node) {
return dojo.hasClass(node,'selected');
};
// update from the My Tracks pseudofacet
(function() {
var mytracks_options = this.facetSelectors['My Tracks'];
// index the optoins by name
var byname = {};
dojo.forEach( mytracks_options, function(opt){ byname[opt.facetValue] = opt;});
// if filtering for active tracks, add the labels for the
// currently selected tracks to the query
if( is_selected( byname['Currently Active'] ) ) {
var activeTrackLabels = dojof.keys(this.tracksActive || {});
newQuery.label = Util.uniq(
(newQuery.label ||[])
.concat( activeTrackLabels )
);
}
// if filtering for recently used tracks, add the labels of recently used tracks
if( is_selected( byname['Recently Used'])) {
var recentlyUsed = dojo.map(
this.browser.getRecentlyUsedTracks(),
function(t){
return t.label;
}
);
newQuery.label = Util.uniq(
(newQuery.label ||[])
.concat(recentlyUsed)
);
}
// finally, if something is selected in here, but we have
// not come up with any track labels, then insert a dummy
// track label value that will never match, because the
// query engine ignores empty arrayrefs.
if( ( ! newQuery.label || ! newQuery.label.length )
&& array.some( mytracks_options, is_selected )
) {
newQuery.label = ['FAKE LABEL THAT IS HIGHLY UNLIKELY TO EVER MATCH ANYTHING'];
}
}).call(this);
// update from the text filter
if( this.textFilterInput.value.length ) {
newQuery.text = this.textFilterInput.value;
}
// update from the data-based facet selectors
dojo.forEach( this.trackDataStore.getFacetNames(), function(facetName) {
var options = this.facetSelectors[facetName];
if( !options ) return;
var selectedFacets = dojo.map(
dojo.filter( options, is_selected ),
function(opt) {return opt.facetValue;}
);
if( selectedFacets.length )
newQuery[facetName] = selectedFacets;
},this);
this.query = newQuery;
this.dataGrid.setQuery( this.query );
this._updateMatchCount();
},
/**
* Update the match-count text in the grid controls bar based
* on the last query that was run against the store.
* @private
*/
_updateMatchCount: function() {
var count = this.dataGrid.store.getCount();
dojo.query( '.matching_record_count', this.containerElem )
.forEach( function(n) {
n.innerHTML =
Util.addCommas(count)
+ ' '+( dojof.keys(this.query||{}).length ? 'matching ' : '' )
+'track' + ( count == 1 ? '' : 's' );
},
this
);
},
/**
* Update the grid to have only rows checked that correspond to
* tracks that are currently active.
* @private
*/
_updateGridSelections: function() {
this.ready.then(() => {
// keep selection events from firing while we mess with the
// grid
this._ifNotSuppressed(['gridUpdate','selectionEvents'], function(){
this._suppress('selectionEvents', function() {
this.dataGrid.selection.deselectAll();
// check the boxes that should be checked, based on our
// internal memory of what tracks should be on.
for( var i= 0; i < Math.min( this.dataGrid.get('rowCount'), this.dataGrid.get('rowsPerPage') ); i++ ) {
var item = this.dataGrid.getItem( i );
if( item ) {
var label = this.dataGrid.store.getIdentity( item );
if( this.tracksActive[label] )
this.dataGrid.rowSelectCell.toggleRow( i, true );
}
}
});
});
})
},
/**
* Given an array of track configs, update the track list to show
* that they are turned on.
*/
setTracksActive: function( /**Array[Object]*/ trackConfigs ) {
dojo.forEach( trackConfigs, function(conf) {
this.tracksActive[conf.label] = true;
},this);
this._updateGridSelections();
},
/**
* Given an array of track configs, update the track list to show
* that they are turned off.
*/
setTracksInactive: function( /**Array[Object]*/ trackConfigs ) {
dojo.forEach( trackConfigs, function(conf) {
delete this.tracksActive[conf.label];
},this);
this._updateGridSelections();
},
/**
* Make the track selector visible.
*/
show: function() {
window.setTimeout( lang.hitch( this, function() {
this.textFilterInput.disabled = false;
this.textFilterInput.focus();
}), 300);
dojo.addClass( this.containerElem, 'active' );
dojo.animateProperty({
node: this.containerElem,
properties: {
left: { start: -95, end: 0, units: '%' }
}
}).play();
this.shown = true;
},
/**
* Make the track selector invisible.
*/
hide: function() {
dojo.removeClass( this.containerElem, 'active' );
dojo.animateProperty({
node: this.containerElem,
properties: {
left: { start: 0, end: -95, units: '%' }
}
}).play();
this.textFilterInput.blur();
this.textFilterInput.disabled = true;
this.shown = false;
},
/**
* Toggle whether the track selector is visible.
*/
toggle: function() {
this.shown ? this.hide() : this.show();
}
});
});