UI, Dataset choice control: add DatasetChoiceModal for choosing single or multiple datasets, add DatasetChoice and MultiDatasetChoice for displaying and activating selection modal from within a page; correct lookup of galaxy_config in dataset-model; add ListPanel for displaying collections of ListViews; add DatasetListPanel for displaying collections of generic datasets; add default no-op localizer (_l) to ui.js until that file is AMD; factor out icon-btn.less from history.less; remove make_popup_menus in default expandable behaviors

This commit is contained in:
Carl Eberhard
2014-08-26 12:59:15 -04:00
parent 4c69734c3f
commit 8a9b24dcd4
17 changed files with 1677 additions and 107 deletions
+5 -3
View File
@@ -1,6 +1,7 @@
define([
'utils/add-logging'
], function( addLogging ){
'utils/add-logging',
'utils/localization'
], function( addLogging, _l ){
//ASSUMES: backbone
//==============================================================================
/** @class Mixin to add logging capabilities to an object.
@@ -390,6 +391,7 @@ var ExpandableView = Backbone.View.extend( LoggableMixin ).extend({
/** are the details of this view expanded/shown or not? */
this.expanded = attributes.expanded || false;
//this.log( '\t expanded:', this.expanded );
this.fxSpeed = attributes.fxSpeed || this.fxSpeed;
},
// ........................................................................ render main
@@ -451,7 +453,7 @@ var ExpandableView = Backbone.View.extend( LoggableMixin ).extend({
_setUpBehaviors : function( $where ){
$where = $where || this.$el;
// set up canned behavior on children (bootstrap, popupmenus, editable_text, etc.)
make_popup_menus( $where );
//make_popup_menus( $where );
$where.find( '[title]' ).tooltip({ placement : 'bottom' });
},
@@ -0,0 +1,442 @@
define([
'mvc/dataset/dataset-model',
'mvc/dataset/dataset-list',
'mvc/ui/ui-modal',
'mvc/base-mvc',
'utils/localization'
], function( DATASET, DATASET_LIST, MODAL, BASE_MVC, _l ){
/* ============================================================================
TODO:
does this really work with mixed contents?
Single dataset choice: allow none?
tooltips rendered *behind* modal
collection selector
better handling when no results returned from filterDatasetJSON
pass optional subtitle from choice display to modal
onfirstclick
drop target
return modal with promise?
auto showing the modal may not be best
add hidden inputs
// cut1 on single dataset (17 in the list)
__switch_default__ select_single
input 17
// cut1 on two datasets
__switch_default__ select_single
input|__multirun__ 13
input|__multirun__ 15
// cut1 on a collection
__switch_default__ select_collection
input|__collection_multirun__ f2db41e1fa331b3e
============================================================================ */
/** Filters an array of dataset plain JSON objs.
*/
function _filterDatasetJSON( datasetJSON, where, datasetsOnly ){
//TODO: replace with _.matches (underscore 1.6.0)
function matches( obj, toMatch ){
for( var key in toMatch ){
if( toMatch.hasOwnProperty( key ) ){
if( obj[ key ] !== toMatch[ key ] ){
return false;
}
}
}
return true;
}
return datasetJSON.filter( function( json ){
return ( !json.deleted && json.visible )
&& ( !datasetsOnly || json.collection_type === undefined )
&& ( matches( json, where ) );
});
}
// ============================================================================
/** Given an array of plain JSON objs rep. datasets, show a modal allowing a choice
* of one or more of those datasets.
*
* Pass:
* an array of plain JSON objects representing allowed dataset choices
* a map of options (see below)
*
* Options:
* datasetsOnly: T: display only datasets, F: datasets + dataset collections
* where: a map of attributes available choices *must have* (defaults to { state: 'ok' })
* multiselect: T: user can select more than one, F: only one
* selected: array of dataset ids to make them selected by default
*
* @example:
* var datasetJSON = // from ajax or bootstrap
* // returns a jQuery promise (that 'fail's only if no datasets are found matching 'where' below)
* var choice = new DatasetChoiceModal( datasetJSON, {
* datasetsOnly : false,
* where : { state: 'ok', data_type: 'bed', ... },
* multiselect : true,
* selected : [ 'df7a1f0c02a5b08e', 'abcdef0123456789' ]
*
* }).done( function( json ){
* if( json ){
* console.debug( json );
* // returned choice will always be an array (single or multi)
* // [{ <selected dataset JSON 1>, <selected dataset JSON 2>, ... }]
* // ... do stuff
* } else {
* // json will === null if the user cancelled selection
* console.debug( 'cancelled' );
* }
* });
*/
var DatasetChoiceModal = function( datasetJSON, options ){
// option defaults
options = _.defaults( options || {}, {
// show datasets or datasets and collections
datasetsOnly : true,
// map of attributes to filter datasetJSON by
where : { state: 'ok' },
// select more than one dataset?
multiselect : false,
// any dataset ids that will display as already selected
selected : []
});
// default title should depend on multiselect
options.title = options.title ||
( options.multiselect? _l( 'Choose datasets:' ): _l( 'Choose a dataset:' ) );
var modal, list, buttons,
promise = jQuery.Deferred(),
filterFn = options.filter || _filterDatasetJSON;
// filter the given datasets and if none left return a rejected promise for use with fail()
datasetJSON = filterFn( datasetJSON, options.where, options.datasetsOnly );
if( !datasetJSON.length ){
return promise.reject( 'No matches found' );
}
// resolve the returned promise with the json of the selected datasets
function resolveWithSelected(){
promise.resolve( list.getSelectedModels().map( function( model ){
return model.toJSON();
}));
}
// if multiselect - add a button for the user to complete the changes
if( options.multiselect ){
buttons = {};
buttons[ _l( 'Ok' ) ] = resolveWithSelected;
}
// create a full-height modal that's cancellable, remove unneeded elements and styles
modal = new MODAL.View({
height : 'auto',
buttons : buttons,
closing_events : true,
closing_callback : function(){ promise.resolve( null ); },
body : [
'<div class="list-panel"></div>'
].join('')
});
modal.$( '.modal-header' ).remove();
modal.$( '.modal-footer' ).css( 'margin-top', '0px' );
// attach a dataset list (of the filtered datasets) to that modal that's selectable
list = new DATASET_LIST.DatasetList({
title : options.title,
subtitle : options.subtitle || _l([
//TODO: as option
'Click the checkboxes on the right to select datasets. ',
'Click the datasets names to see their details. '
].join('')),
el : modal.$body.find( '.list-panel' ),
selecting : true,
selected : options.selected,
collection : new DATASET.DatasetAssociationCollection( datasetJSON )
});
// when the list is rendered, show the modal (also add a specifying class for css)
list.once( 'rendered:initial', function(){
modal.show();
modal.$el.addClass( 'dataset-choice-modal' );
});
if( !options.multiselect ){
// if single select, remove the all/none list actions from the panel
list.on( 'rendered', function(){
list.$( '.list-actions' ).hide();
});
// if single select, immediately resolve on a single selection
list.on( 'view:selected', function( view ){
promise.resolve([ view.model.toJSON() ]);
});
}
list.render( 0 );
// return the promise, and on any resolution close the modal
return promise.always( function(){
modal.hide();
});
};
// ============================================================================
/** Activator for single dataset selection modal and display of the selected dataset.
* The activator/display will show as a single div and, when a dataset is selected,
* show the name and details of the selected dataset.
*
* When clicked the div will generate a DatasetChoiceModal of the available choices.
*
* Options:
* datasetJSON: array of plain json objects representing allowed choices
* datasetsOnly: T: only show datasets in the allowed choices, F: datasets + collections
* where: map of attributes to filter datasetJSON by (e.g. { data_type: 'bed' })
* label: the label/prompt displayed
* selected: array of dataset ids that will show as already selected in the control
*
* @example:
* var choice1 = new DATASET_CHOICE.DatasetChoice({
* datasetJSON : datasetJSON,
* label : 'Input dataset',
* selected : [ 'df7a1f0c02a5b08e' ]
* });
* $( 'body' ).append( choice1.render().$el )
*
* Listen to the DatasetChoice to react to changes in the user's choice/selection:
* @example:
* choice1.on( 'selected', function( chooser, selectionJSONArray ){
* // ... do stuff with new selections
* });
*/
var DatasetChoice = Backbone.View.extend( BASE_MVC.LoggableMixin ).extend({
//logger : console,
className : 'dataset-choice',
/** set up defaults, options, and listeners */
initialize : function( attributes ){
this.debug( this + '(DatasetChoice).initialize:', attributes );
this.label = attributes.label !== undefined? _l( attributes.label ) : '';
this.where = attributes.where;
this.datasetsOnly = attributes.datasetsOnly !== undefined? attributes.datasetsOnly: true;
this.datasetJSON = attributes.datasetJSON || [];
this.selected = attributes.selected || [];
this._setUpListeners();
},
/** add any (bbone) listeners */
_setUpListeners : function(){
//this.on( 'all', function(){
// this.log( this + '', arguments );
//});
},
/** render the view */
render : function(){
var json = this.toJSON();
this.$el.html( this._template( json ) );
this.$( '.selected' ).replaceWith( this._renderSelected( json ) );
return this;
},
/** return plain html for the overall control */
_template : function( json ){
return _.template([
'<label>',
'<span class="prompt"><%= json.label %></span>',
'<div class="selected"></div>',
'</label>'
].join(''), { json: json });
},
/** return jQ DOM for the selected dataset (only one) */
_renderSelected : function( json ){
if( json.selected.length ){
//TODO: break out?
return $( _.template([
'<div class="selected">',
'<span class="title"><%= selected.hid %>: <%= selected.name %></span>',
'<span class="subtitle">',
'<i><%= selected.misc_blurb %></i>',
'<i>', _l( 'format' ) + ': ', '<%= selected.data_type %></i>',
'<i><%= selected.misc_info %></i>',
'</span>',
'</div>'
].join( '' ), { selected: json.selected[0] }));
}
return $([
'<span class="none-selected-msg">(',
_l( 'click to select a dataset' ),
')</span>'
].join( '' ));
},
//TODO:?? why not just pass in view?
/** return a plain JSON object with both the view and dataset attributes */
toJSON : function(){
var chooser = this;
return {
label : chooser.label,
datasets : chooser.datasetJSON,
selected : _.compact( _.map( chooser.selected, function( id ){
return _.findWhere( chooser.datasetJSON, { id: id });
}))
};
},
/** event map: when to open the modal */
events : {
// the whole thing functions as a button
'click' : 'chooseWithModal'
},
//TODO:?? modal to prop of this?
//TODO:?? should be able to handle 'none selectable' on initialize
/** open the modal and handle the promise representing the user's choice
* @fires 'selected' when the user selects dataset(s) - passed full json of the selected datasets
* @fires 'cancelled' when the user clicks away/closes the modal (no selection made) - passed this
* @fires 'error' if the modal has no selectable datasets based on this.where - passed this and other args
*/
chooseWithModal : function(){
var chooser = this;
return this._createModal()
.done( function( json ){
if( json ){
chooser.selected = _.pluck( json, 'id' );
chooser.trigger( 'selected', chooser, json );
chooser.render();
} else {
chooser.trigger( 'cancelled', chooser );
}
})
.fail( function(){
chooser.trigger( 'error', chooser, arguments );
});
},
/** create and return the modal to use for choosing */
_createModal : function(){
return new DatasetChoiceModal( this.datasetJSON, this._getModalOptions() );
},
/** return a plain JSON containing the options to pass to the modal */
_getModalOptions : function(){
return {
title : this.label,
multiselect : false,
selected : this.selected,
where : this.where,
datasetsOnly : this.datasetsOnly
};
},
// ------------------------------------------------------------------------ misc
/** string rep */
toString : function(){
return 'DatasetChoice(' + this.selected + ')';
}
});
// ============================================================================
/** Activator for multiple dataset selection modal and display of the selected datasets.
* The activator/display will show as a table of all choices.
*
* See DatasetChoice (above) for example usage.
*
* Additional options:
* showHeaders: T: show headers for selected dataset attributes in the display table
* cells: map of attribute keys -> Human readable/localized column headers
* (e.g. { data_type: _l( 'Format' ) }) - defaults are listed below
*/
var MultiDatasetChoice = DatasetChoice.extend({
className : DatasetChoice.prototype.className + ' multi',
/** default (dataset attribute key -> table header text) map of what cells to display in the table */
cells : {
hid : _l( 'History #' ),
name : _l( 'Name' ),
misc_blurb : _l( 'Summary' ),
data_type : _l( 'Format' ),
genome_build : _l( 'Genome' ),
tags : _l( 'Tags' ),
annotation : _l( 'Annotation' )
},
/** in this override, add the showHeaders and cells options */
initialize : function( attributes ){
this.showHeaders = attributes.showHeaders !== undefined? attributes.showHeaders : true;
this.cells = attributes.cells || this.cells;
DatasetChoice.prototype.initialize.call( this, attributes );
},
/** in this override, display the selected datasets as a table with optional headers */
_renderSelected : function( json ){
if( json.selected.length ){
return $( _.template([
'<table class="selected">',
'<% if( json.showHeaders ){ %>',
'<thead><tr>',
'<% _.map( json.cells, function( val, key ){ %>',
'<th><%= val %></th>',
'<% }); %>',
'</tr></thead>',
'<% } %>',
'<tbody>',
'<% _.map( json.selected, function( selected ){ %>',
'<tr>',
'<% _.map( json.cells, function( val, key ){ %>',
'<td class="cell-<%= key %>"><%= selected[ key ] %></td>',
'<% }) %>',
'</tr>',
'<% }); %>',
'</tbody>',
'</table>'
].join( '' ), { json: json }));
}
return $([
'<span class="none-selected-msg">(',
_l( 'click to select a dataset' ),
')</span>'
].join( '' ));
},
/** in this override, send the showHeaders and cells options as well */
toJSON : function(){
return _.extend( DatasetChoice.prototype.toJSON.call( this ), {
showHeaders : this.showHeaders,
cells : this.cells
});
},
/** in this override, set multiselect to true */
_getModalOptions : function(){
return _.extend( DatasetChoice.prototype._getModalOptions.call( this ), {
multiselect : true
});
},
// ------------------------------------------------------------------------ misc
/** string rep */
toString : function(){
return 'DatasetChoice(' + this.selected + ')';
}
});
// ============================================================================
return {
DatasetChoiceModal : DatasetChoiceModal,
DatasetChoice : DatasetChoice,
MultiDatasetChoice : MultiDatasetChoice
};
});
+2 -2
View File
@@ -42,14 +42,14 @@ var DatasetListItemView = _super.extend(
/** event listeners */
_setUpListeners : function(){
//TODO:?? may want to move this to ListItemView (although this is only needed in the *narrow* panel views (current))
// hide the primary actions in the title bar when selectable
// hide the primary actions in the title bar when selectable and narrow
this.on( 'selectable', function( isSelectable ){
if( isSelectable ){
this.$( '.primary-actions' ).hide();
} else {
this.$( '.primary-actions' ).show();
}
});
}, this );
// re-rendering on any model changes
this.model.on( 'change', function( model, options ){
@@ -0,0 +1,45 @@
define([
"mvc/dataset/list-panel",
"mvc/dataset/dataset-li",
"mvc/base-mvc",
"utils/localization"
], function( LIST_PANEL, DATASET_LI, BASE_MVC, _l ){
/* =============================================================================
TODO:
============================================================================= */
var _super = LIST_PANEL.ListPanel;
/** @class non-editable, read-only View/Controller for a list of datasets.
*/
var DatasetList = _super.extend(
/** @lends DatasetList.prototype */{
/** logger used to record this.log messages, commonly set to console */
//logger : console,
/** class to use for constructing the sub-views */
viewClass : DATASET_LI.DatasetListItemView,
className : _super.prototype.className + ' dataset-list',
/** string to no hdas match the search terms */
noneFoundMsg : _l( 'No matching datasets found' ),
// ......................................................................... SET UP
/** Set up the view, set up storage, bind listeners to HistoryContents events
* @param {Object} attributes optional settings for the panel
*/
initialize : function( attributes ){
_super.prototype.initialize.call( this, attributes );
},
/** Return a string rep of the history */
toString : function(){
return 'DatasetList(' + this.collection + ')';
}
});
//==============================================================================
return {
DatasetList : DatasetList
};
});
+205 -2
View File
@@ -75,7 +75,7 @@ var DatasetAssociation = Backbone.Model.extend( BASE_MVC.LoggableMixin ).extend(
'meta_download' : 'dataset/get_metadata_file?hda_id=' + id + '&metadata_name='
};
//TODO: global
var root = ( galaxy_config && galaxy_config.root )?( galaxy_config.root ):( '/' );
var root = ( window.galaxy_config && galaxy_config.root )?( galaxy_config.root ):( '/' );
_.each( urls, function( value, key ){
urls[ key ] = root + value;
});
@@ -228,8 +228,211 @@ var DatasetAssociation = Backbone.Model.extend( BASE_MVC.LoggableMixin ).extend(
}));
//==============================================================================
/** @class Backbone collection for dataset associations.
*/
var DatasetAssociationCollection = Backbone.Collection.extend( BASE_MVC.LoggableMixin ).extend(
/** @lends HistoryContents.prototype */{
model : DatasetAssociation,
/** logger used to record this.log messages, commonly set to console */
//logger : console,
/** root api url */
urlRoot : (( window.galaxy_config && galaxy_config.root )?( galaxy_config.root ):( '/' ))
+ 'api/datasets',
// ........................................................................ common queries
/** Get the ids of every item in this collection
* @returns array of encoded ids
*/
ids : function(){
return this.map( function( item ){ return item.get('id'); });
},
/** Get contents that are not ready
* @returns array of content models
*/
notReady : function(){
return this.filter( function( content ){
return !content.inReadyState();
});
},
// /** Get the id of every model in this collection not in a 'ready' state (running).
// * @returns an array of model ids
// */
// running : function(){
// var idList = [];
// this.each( function( item ){
// var isRunning = !item.inReadyState();
// if( isRunning ){
////TODO: is this still correct since type_id
// idList.push( item.get( 'id' ) );
// }
// });
// return idList;
// },
/** return true if any datasets don't have details */
haveDetails : function(){
return this.all( function( dataset ){ return dataset.hasDetails(); });
},
// ........................................................................ ajax
///** fetch detailed model data for all datasets in this collection */
//fetchAllDetails : function( options ){
// options = options || {};
// var detailsFlag = { details: 'all' };
// options.data = ( options.data )?( _.extend( options.data, detailsFlag ) ):( detailsFlag );
// return this.fetch( options );
//},
/** using a queue, perform ajaxFn on each of the models in this collection */
ajaxQueue : function( ajaxFn, options ){
var deferred = jQuery.Deferred(),
startingLength = this.length,
responses = [];
if( !startingLength ){
deferred.resolve([]);
return deferred;
}
// use reverse order (stylistic choice)
var ajaxFns = this.chain().reverse().map( function( dataset, i ){
return function(){
var xhr = ajaxFn.call( dataset, options );
// if successful, notify using the deferred to allow tracking progress
xhr.done( function( response ){
deferred.notify({ curr: i, total: startingLength, response: response, model: dataset });
});
// (regardless of previous error or success) if not last ajax call, shift and call the next
// if last fn, resolve deferred
xhr.always( function( response ){
responses.push( response );
if( ajaxFns.length ){
ajaxFns.shift()();
} else {
deferred.resolve( responses );
}
});
};
}).value();
// start the queue
ajaxFns.shift()();
return deferred;
},
// ........................................................................ sorting/filtering
/** return a new collection of datasets whose attributes contain the substring matchesWhat */
matches : function( matchesWhat ){
return this.filter( function( dataset ){
return dataset.matches( matchesWhat );
});
},
// ........................................................................ misc
/** override to get a correct/smarter merge when incoming data is partial */
set : function( models, options ){
// arrrrrrrrrrrrrrrrrg...
// (e.g. stupid backbone)
// w/o this partial models from the server will fill in missing data with model defaults
// and overwrite existing data on the client
// see Backbone.Collection.set and _prepareModel
var collection = this;
models = _.map( models, function( model ){
if( !collection.get( model.id ) ){ return model; }
// merge the models _BEFORE_ calling the superclass version
var merged = existing.toJSON();
_.extend( merged, model );
return merged;
});
// now call superclass when the data is filled
Backbone.Collection.prototype.set.call( this, models, options );
},
// /** Convert this ad-hoc collection of hdas to a formal collection tracked
// by the server.
// **/
// promoteToHistoryDatasetCollection : function _promote( history, collection_type, options ){
////TODO: seems like this would be better in mvc/collections
// options = options || {};
// options.url = this.url();
// options.type = "POST";
// var full_collection_type = collection_type;
// var element_identifiers = [],
// name = null;
//
// // This mechanism is rough - no error handling, allows invalid selections, no way
// // for user to pick/override element identifiers. This is only really meant
// if( collection_type === "list" ) {
// this.chain().each( function( hda ) {
// // TODO: Handle duplicate names.
// var name = hda.attributes.name;
// var id = hda.get('id');
// var content_type = hda.attributes.history_content_type;
// if( content_type === "dataset" ) {
// if( full_collection_type !== "list" ) {
// this.log( "Invalid collection type" );
// }
// element_identifiers.push( { name: name, src: "hda", id: id } );
// } else {
// if( full_collection_type === "list" ) {
// full_collection_type = "list:" + hda.attributes.collection_type;
// } else {
// if( full_collection_type !== "list:" + hda.attributes.collection_type ) {
// this.log( "Invalid collection type" );
// }
// }
// element_identifiers.push( { name: name, src: "hdca", id: id } );
// }
// });
// name = "New Dataset List";
// } else if( collection_type === "paired" ) {
// var ids = this.ids();
// if( ids.length !== 2 ){
// // TODO: Do something...
// }
// element_identifiers.push( { name: "forward", src: "hda", id: ids[ 0 ] } );
// element_identifiers.push( { name: "reverse", src: "hda", id: ids[ 1 ] } );
// name = "New Dataset Pair";
// }
// options.data = {
// type: "dataset_collection",
// name: name,
// collection_type: full_collection_type,
// element_identifiers: JSON.stringify( element_identifiers )
// };
//
// var xhr = jQuery.ajax( options );
// xhr.done( function( message, status, responseObj ){
// history.refresh( );
// });
// xhr.fail( function( xhr, status, message ){
// if( xhr.responseJSON && xhr.responseJSON.error ){
// error = xhr.responseJSON.error;
// } else {
// error = xhr.responseJSON;
// }
// xhr.responseText = error;
// // Do something?
// });
// return xhr;
// },
/** String representation. */
toString : function(){
return ([ 'DatasetAssociationCollection(', this.length, ')' ].join( '' ));
}
});
//==============================================================================
return {
DatasetAssociation : DatasetAssociation
DatasetAssociation : DatasetAssociation,
DatasetAssociationCollection : DatasetAssociationCollection
};
});
+687
View File
@@ -0,0 +1,687 @@
define([
"mvc/base-mvc",
"utils/localization"
], function( BASE_MVC, _l ){
/* =============================================================================
TODO:
============================================================================= */
/** @class List that contains ListItemViews.
*/
var ListPanel = Backbone.View.extend( BASE_MVC.LoggableMixin ).extend(
/** @lends ReadOnlyHistoryPanel.prototype */{
/** logger used to record this.log messages, commonly set to console */
//logger : console,
/** class to use for constructing the sub-views */
viewClass : BASE_MVC.ListItemView,
tagName : 'div',
className : 'list-panel',
/** (in ms) that jquery effects will use */
fxSpeed : 'fast',
/** string to display when the model has no hdas */
emptyMsg : _l( 'This list is empty' ),
/** string to no hdas match the search terms */
noneFoundMsg : _l( 'No matching items found' ),
// ......................................................................... SET UP
/** Set up the view, set up storage, bind listeners to HistoryContents events
* @param {Object} attributes optional settings for the list
*/
initialize : function( attributes, options ){
attributes = attributes || {};
// set the logger if requested
if( attributes.logger ){
this.logger = attributes.logger;
}
this.log( this + '.initialize:', attributes );
// ---- instance vars
/** how quickly should jquery fx run? */
this.fxSpeed = _.has( attributes, 'fxSpeed' )?( attributes.fxSpeed ):( this.fxSpeed );
/** filters for displaying subviews */
this.filters = [];
/** current search terms */
this.searchFor = attributes.searchFor || '';
/** loading indicator */
this.indicator = new LoadingIndicator( this.$el );
/** currently showing selectors on items? */
this.selecting = ( attributes.selecting !== undefined )? attributes.selecting : true;
//this.selecting = false;
/** cached selected item.model.ids to persist btwn renders */
this.selected = attributes.selected || [];
/** the last selected item.model.id */
this.lastSelected = null;
/** list item view class (when passed models) */
this.viewClass = attributes.viewClass || this.viewClass;
/** list item views */
this.views = [];
/** list item models */
this.collection = attributes.collection || ( new Backbone.Collection([]) );
/** filter fns run over collection items to see if they should show in the list */
this.filters = attributes.filters || [];
//TODO: remove
this.title = attributes.title || '';
this.subtitle = attributes.subtitle || '';
this._setUpListeners();
},
/** create any event listeners for the list
*/
_setUpListeners : function(){
this.on( 'error', function( model, xhr, options, msg, details ){
//this.errorHandler( model, xhr, options, msg, details );
console.error( model, xhr, options, msg, details );
}, this );
// show hide the loading indicator
this.on( 'loading', function(){
this._showLoadingIndicator( 'loading...', 40 );
}, this );
this.on( 'loading-done', function(){
this._hideLoadingIndicator( 40 );
}, this );
// throw the first render up as a diff namespace using once (for outside consumption)
this.once( 'rendered', function(){
this.trigger( 'rendered:initial', this );
}, this );
// debugging
if( this.logger ){
this.on( 'all', function( event ){
this.log( this + '', arguments );
}, this );
}
this._setUpCollectionListeners();
this._setUpViewListeners();
return this;
},
/** free any sub-views the list has */
freeViews : function(){
//TODO: stopListening? remove?
this.views = [];
return this;
},
// ------------------------------------------------------------------------ item listeners
/** listening for history and HDA events */
_setUpCollectionListeners : function(){
this.collection.on( 'reset', function(){
this.renderItems();
}, this );
this.collection.on( 'add', this.addItemView, this );
this.collection.on( 'remove', this.removeItemView, this );
// debugging
if( this.logger ){
this.collection.on( 'all', function( event ){
this.info( this + '(collection)', arguments );
}, this );
}
return this;
},
/** listening for history and HDA events */
_setUpViewListeners : function(){
// shift to select a range
this.on( 'view:selected', function( view, ev ){
if( ev && ev.shiftKey && this.lastSelected ){
var lastSelectedView = _.find( this.views, function( view ){
return view.model.id === this.lastSelected;
});
if( lastSelectedView ){
this.selectRange( view, lastSelectedView );
}
}
this.selected.push( view.model.id );
this.lastSelected = view.model.id;
}, this );
},
// ------------------------------------------------------------------------ rendering
/** Render this content, set up ui.
* @param {Number or String} speed the speed of the render
*/
render : function( speed ){
var $newRender = this._buildNewRender();
this._setUpBehaviors( $newRender );
this._queueNewRender( $newRender, speed );
return this;
},
/** Build a temp div containing the new children for the view's $el.
*/
_buildNewRender : function(){
// create a new render using a skeleton template, render title buttons, render body, and set up events, etc.
var json = this.model? this.model.toJSON() : {},
$newRender = $( this.templates.el( json, this ) );
this._renderTitle( $newRender );
this._renderSubtitle( $newRender );
this._renderSearch( $newRender );
this.renderItems( $newRender );
return $newRender;
},
/**
*/
_renderTitle : function( $where ){
//$where = $where || this.$el;
//$where.find( '.title' ).replaceWith( ... )
},
/**
*/
_renderSubtitle : function( $where ){
//$where = $where || this.$el;
//$where.find( '.title' ).replaceWith( ... )
},
/** Fade out the old el, swap in the new contents, then fade in.
* @param {Number or String} speed jq speed to use for rendering effects
* @fires rendered when rendered
*/
_queueNewRender : function( $newRender, speed ) {
speed = ( speed === undefined )?( this.fxSpeed ):( speed );
var view = this;
$( view ).queue( 'fx', [
function( next ){ this.$el.fadeOut( speed, next ); },
function( next ){
view._swapNewRender( $newRender );
next();
},
function( next ){ this.$el.fadeIn( speed, next ); },
function( next ){
view.trigger( 'rendered', view );
next();
}
]);
},
/** empty out the current el, move the $newRender's children in */
_swapNewRender : function( $newRender ){
this.$el.empty().attr( 'class', this.className ).append( $newRender.children() );
if( this.selecting ){ this.showSelectors( 0 ); }
return this;
},
/** */
_setUpBehaviors : function( $where ){
$where = $where || this.$el;
$where.find( '.controls [title]' ).tooltip({ placement: 'bottom' });
return this;
},
// ------------------------------------------------------------------------ sub-$element shortcuts
/** the scroll container for this panel - can be $el, $el.parent(), or grandparent depending on context */
$scrollContainer : function(){
// override
return this.$el.parent().parent();
},
/** */
$list : function( $where ){
return ( $where || this.$el ).find( '.list-items' );
},
/** container where list messages are attached */
$messages : function( $where ){
return ( $where || this.$el ).find( '.message-container' );
},
/** the message displayed when no views can be shown (no views, none matching search) */
$emptyMessage : function( $where ){
return ( $where || this.$el ).find( '.empty-message' );
},
// ------------------------------------------------------------------------ hda sub-views
/**
* @param {jQuery} $whereTo what dom element to prepend the HDA views to
* @returns the visible item views
*/
renderItems : function( $whereTo ){
$whereTo = $whereTo || this.$el;
var list = this,
newViews = [];
var $list = this.$list( $whereTo ),
item$els = this._filterCollection().map( function( itemModel ){
//TODO: creates views each time - not neccessarily good
var view = list._createItemView( itemModel );
newViews.push( view );
return view.render( 0 ).$el;
});
this.debug( item$els );
this.debug( newViews );
$list.empty();
if( item$els.length ){
$list.append( item$els );
this.$emptyMessage( $whereTo ).hide();
} else {
this._renderEmptyMessage( $whereTo ).show();
}
this.views = newViews;
return newViews;
},
/**
*/
_filterCollection : function(){
// override this
var list = this;
return list.collection.filter( _.bind( list._filterItem, list ) );
},
/**
*/
_filterItem : function( model ){
// override this
var list = this;
return ( _.every( list.filters.map( function( fn ){ return fn.call( model ); }) ) )
&& ( !list.searchFor || model.matchesAll( list.searchFor ) );
},
/**
*/
_createItemView : function( model ){
var ViewClass = this._getItemViewClass( model ),
options = _.extend( this._getItemViewOptions( model ), {
model : model
}),
view = new ViewClass( options );
this._setUpItemViewListeners( view );
return view;
},
_getItemViewClass : function( model ){
// override this
return this.viewClass;
},
_getItemViewOptions : function( model ){
// override this
return {
//logger : this.logger,
fxSpeed : this.fxSpeed,
expanded : false,
selectable : this.selecting,
selected : _.contains( this.selected, model.id ),
draggable : this.dragging
};
},
/**
*/
_setUpItemViewListeners : function( view ){
var list = this;
view.on( 'all', function(){
var args = Array.prototype.slice.call( arguments, 0 );
args[0] = 'view:' + args[0];
list.trigger.apply( list, args );
});
// debugging
//if( this.logger ){
// view.on( 'all', function( event ){
// this.log( this + '(view)', arguments );
// }, this );
//}
return this;
},
/** render the empty/none-found message */
_renderEmptyMessage : function( $whereTo ){
//this.debug( '_renderEmptyMessage', $whereTo, this.searchFor );
var text = this.searchFor? this.noneFoundMsg : this.emptyMsg;
return this.$emptyMessage( $whereTo ).text( text );
},
/** collapse all item views */
expandAll : function(){
_.each( this.views, function( view ){
view.expand();
});
},
/** collapse all item views */
collapseAll : function(){
_.each( this.views, function( view ){
view.collapse();
});
},
// ------------------------------------------------------------------------ collection/views syncing
/**
*/
addItemView : function( model, collection, options ){
this.log( this + '.addItemView:', model );
var list = this;
if( !this._filterItem( model ) ){ return undefined; }
//TODO: sorted? position?
var view = list._createItemView( model );
this.views.push( view );
$( view ).queue( 'fx', [
function( next ){ list.$emptyMessage().fadeOut( list.fxSpeed, next ); },
function( next ){
//TODO: auto render?
list.$list().append( view.render().$el );
next();
}
]);
return view;
},
/**
*/
removeItemView : function( model, collection, options ){
this.log( this + '.removeItemView:', model );
var list = this,
view = list.viewFromModel( model );
if( !view ){ return undefined; }
this.views = _.without( this.views, view );
view.remove();
if( !this.views.length ){
list._renderEmptyMessage().fadeIn( list.fxSpeed );
}
return view;
},
/** get views based on model properties
*/
viewFromModel : function( model ){
for( var i=0; i<this.views.length; i++ ){
var view = this.views[i];
if( view.model === model ){
return view;
}
}
return undefined;
},
/** get views based on model properties
*/
viewsWhereModel : function( properties ){
return this.views.filter( function( view ){
//return view.model.matches( properties );
//TODO: replace with _.matches (underscore 1.6.0)
var json = view.model.toJSON();
//console.debug( '\t', json, properties );
for( var key in properties ){
if( properties.hasOwnPropery( key ) ){
//console.debug( '\t\t', json[ key ], view.model.properties[ key ] );
if( json[ key ] !== view.model.properties[ key ] ){
return false;
}
}
}
return true;
});
},
/**
*/
viewRange : function( viewA, viewB ){
if( viewA === viewB ){ return ( viewA )?( [ viewA ] ):( [] ); }
var indexA = this.views.indexOf( viewA ),
indexB = this.views.indexOf( viewB );
// handle not found
if( indexA === -1 || indexB === -1 ){
if( indexA === indexB ){ return []; }
return ( indexA === -1 )?( [ viewB ] ):( [ viewA ] );
}
// reverse if indeces are
//note: end inclusive
return ( indexA < indexB )?
this.views.slice( indexA, indexB + 1 ) :
this.views.slice( indexB, indexA + 1 );
},
// ------------------------------------------------------------------------ searching
/** render a search input for filtering datasets shown
* (see the search section in the HDA model for implementation of the actual searching)
* return will start the search
* esc will clear the search
* clicking the clear button will clear the search
* uses searchInput in ui.js
*/
_renderSearch : function( $where ){
$where.find( '.controls .search-input' ).searchInput({
placeholder : 'search',
initialVal : this.searchFor,
onfirstsearch : _.bind( this._firstSearch, this ),
onsearch : _.bind( this.searchItems, this ),
onclear : _.bind( this.clearSearch, this )
});
return $where;
},
_firstSearch : function( searchFor ){
this.log( 'onFirstSearch', searchFor );
return this.searchItems( searchFor );
},
/** filter view list to those that contain the searchFor terms */
searchItems : function( searchFor ){
this.searchFor = searchFor;
this.trigger( 'search:searching', searchFor, this );
this.renderItems();
return this;
},
/** clear the search filters and show all views that are normally shown */
clearSearch : function( searchFor ){
//this.log( 'onSearchClear', this );
this.searchFor = '';
this.trigger( 'search:clear', this );
this.renderItems();
return this;
},
// ------------------------------------------------------------------------ selection
/** show selectors on all visible hdas and associated controls */
showSelectors : function( speed ){
speed = ( speed !== undefined )?( speed ):( this.fxSpeed );
this.selecting = true;
this.$( '.list-actions' ).slideDown( speed );
_.each( this.views, function( view ){
view.showSelector( speed );
});
this.selected = [];
this.lastSelected = null;
},
/** hide selectors on all visible hdas and associated controls */
hideSelectors : function( speed ){
speed = ( speed !== undefined )?( speed ):( this.fxSpeed );
this.selecting = false;
this.$( '.list-actions' ).slideUp( speed );
_.each( this.views, function( view ){
view.hideSelector( speed );
});
this.selected = [];
this.lastSelected = null;
},
/** show or hide selectors on all visible hdas and associated controls */
toggleSelectors : function(){
if( !this.selecting ){
this.showSelectors();
} else {
this.hideSelectors();
}
},
/** select all visible hdas */
selectAll : function( event ){
_.each( this.views, function( view ){
view.select( event );
});
},
/** deselect all visible hdas */
deselectAll : function( event ){
this.lastSelected = null;
_.each( this.views, function( view ){
view.deselect( event );
});
},
/** select a range of datasets between A and B */
selectRange : function( viewA, viewB ){
var range = this.viewRange( viewA, viewB );
_.each( range, function( view ){
view.select();
});
return range;
},
/** return an array of all currently selected hdas */
getSelectedViews : function(){
return _.filter( this.views, function( v ){
return v.selected;
});
},
/** return an collection of the models of all currenly selected hdas */
getSelectedModels : function(){
return new this.collection.constructor( _.map( this.getSelectedViews(), function( view ){
return view.model;
}));
},
// ------------------------------------------------------------------------ loading indicator
//TODO: questionable
/** hide the $el and display a loading indicator (in the $el's parent) when loading new data */
_showLoadingIndicator : function( msg, speed, callback ){
speed = ( speed !== undefined )?( speed ):( this.fxSpeed );
if( !this.indicator ){
this.indicator = new LoadingIndicator( this.$el, this.$el.parent() );
}
if( !this.$el.is( ':visible' ) ){
this.indicator.show( 0, callback );
} else {
this.$el.fadeOut( speed );
this.indicator.show( msg, speed, callback );
}
},
/** hide the loading indicator */
_hideLoadingIndicator : function( speed, callback ){
speed = ( speed !== undefined )?( speed ):( this.fxSpeed );
if( this.indicator ){
this.indicator.hide( speed, callback );
}
},
// ------------------------------------------------------------------------ scrolling
/** get the current scroll position of the panel in its parent */
scrollPosition : function(){
return this.$scrollContainer().scrollTop();
},
/** set the current scroll position of the panel in its parent */
scrollTo : function( pos ){
this.$scrollContainer().scrollTop( pos );
return this;
},
/** Scrolls the panel to the top. */
scrollToTop : function(){
this.$scrollContainer().scrollTop( 0 );
return this;
},
/** */
scrollToItem : function( view ){
if( !view ){ return; }
var itemTop = view.$el.offset().top;
this.$scrollContainer().scrollTop( itemTop );
},
// ------------------------------------------------------------------------ panel events
/** event map */
events : {
'click .select-all' : 'selectAll',
'click .deselect-all' : 'deselectAll'
},
// ------------------------------------------------------------------------ misc
/** Return a string rep of the history */
toString : function(){
return 'ListPanel(' + this.collection + ')';
}
});
// ............................................................................ TEMPLATES
/** underscore templates */
ListPanel.prototype.templates = (function(){
//TODO: move to require text! plugin
var elTemplate = BASE_MVC.wrapTemplate([
// temp container
'<div>',
'<div class="controls">',
'<div class="title">',
'<div class="name"><%= model.name || view.title %></div>',
'</div>',
'<div class="subtitle"><%= view.subtitle %></div>',
'<div class="actions"></div>',
'<div class="messages"></div>',
'<div class="search">',
'<div class="search-input"></div>',
'</div>',
'<div class="list-actions">',
'<div class="btn-group">',
'<button class="select-all btn btn-default"',
'data-mode="select">', _l( 'All' ), '</button>',
'<button class="deselect-all btn btn-default"',
'data-mode="select">', _l( 'None' ), '</button>',
'</div>',
//'<button class="action-popup-btn btn btn-default">',
// _l( 'For all selected' ), '...',
//'</button>',
'</div>',
'</div>',
'<div class="list-items"></div>',
'<div class="empty-message infomessagesmall"></div>',
'</div>'
]);
return {
el : elTemplate
};
}());
//==============================================================================
return {
ListPanel: ListPanel
};
});
+6
View File
@@ -610,6 +610,7 @@ function LoadingIndicator( $where, options ){
* of characters, a callback is called. Pressing ESC when the input
* is focused will clear the input and call a separate callback.
*/
var _l = window._l || function( s ){ return s; }
// contructor
function searchInput( parentNode, options ){
@@ -670,6 +671,11 @@ function LoadingIndicator( $where, options ){
})
// attach behaviors to esc, return if desired, search on some min len string
.keyup( function( event ){
event.preventDefault();
event.stopPropagation();
//TODO: doesn't work
if( !$( this ).val() ){ $( this ).blur(); }
// esc key will clear if desired
if( event.which === KEYCODE_ESC && options.escWillClear ){
clearSearchInput.call( this, event );
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
define(["mvc/dataset/states","mvc/base-mvc","utils/localization"],function(a,b,c){var f=b.SearchableModelMixin;var e=Backbone.Model.extend(b.LoggableMixin).extend(b.mixin(f,{defaults:{state:a.NEW,deleted:false,purged:false,name:"(unnamed dataset)",accessible:true,data_type:"",file_ext:"",file_size:0,meta_files:[],misc_blurb:"",misc_info:"",tags:[]},initialize:function(g,h){this.debug(this+"(Dataset).initialize",g,h);if(!this.get("accessible")){this.set("state",a.NOT_VIEWABLE)}this.urls=this._generateUrls();this._setUpListeners()},_generateUrls:function(){var i=this.get("id");if(!i){return{}}var h={purge:"datasets/"+i+"/purge_async",display:"datasets/"+i+"/display/?preview=True",edit:"datasets/"+i+"/edit",download:"datasets/"+i+"/display?to_ext="+this.get("file_ext"),report_error:"dataset/errors?id="+i,rerun:"tool_runner/rerun?id="+i,show_params:"datasets/"+i+"/show_params",visualization:"visualization",meta_download:"dataset/get_metadata_file?hda_id="+i+"&metadata_name="};var g=(galaxy_config&&galaxy_config.root)?(galaxy_config.root):("/");_.each(h,function(k,j){h[j]=g+k});this.urls=h;return h},_setUpListeners:function(){this.on("change:state",function(h,g){this.log(this+" has changed state:",h,g);if(this.inReadyState()){this.trigger("state:ready",h,g,this.previous("state"))}});this.on("change:id change:file_ext",function(g){this._generateUrls()})},toJSON:function(){var g=Backbone.Model.prototype.toJSON.call(this);return _.extend(g,{urls:this.urls})},isDeletedOrPurged:function(){return(this.get("deleted")||this.get("purged"))},inReadyState:function(){var g=_.contains(a.READY_STATES,this.get("state"));return(this.isDeletedOrPurged()||g)},hasDetails:function(){return _.has(this.attributes,"genome_build")},hasData:function(){return(this.get("file_size")>0)},"delete":function(g){if(this.get("deleted")){return jQuery.when()}return this.save({deleted:true},g)},undelete:function(g){if(!this.get("deleted")||this.get("purged")){return jQuery.when()}return this.save({deleted:false},g)},purge:function d(g){if(this.get("purged")){return jQuery.when()}g=g||{};g.url=this.urls.purge;var h=this,i=jQuery.ajax(g);i.done(function(l,j,k){h.set({deleted:true,purged:true})});i.fail(function(n,j,m){var k=c("Unable to purge dataset");var l=("Removal of datasets by users is not allowed in this Galaxy instance");if(n.responseJSON&&n.responseJSON.error){k=n.responseJSON.error}else{if(n.responseText.indexOf(l)!==-1){k=l}}n.responseText=k;h.trigger("error",h,n,g,c(k),{error:k})});return i},searchAttributes:["name","file_ext","genome_build","misc_blurb","misc_info","annotation","tags"],searchAliases:{title:"name",format:"file_ext",database:"genome_build",blurb:"misc_blurb",description:"misc_blurb",info:"misc_info",tag:"tags"},toString:function(){var g=this.get("id")||"";if(this.get("name")){g='"'+this.get("name")+'",'+g}return"Dataset("+g+")"}}));return{DatasetAssociation:e}});
define(["mvc/dataset/states","mvc/base-mvc","utils/localization"],function(a,b,d){var g=b.SearchableModelMixin;var f=Backbone.Model.extend(b.LoggableMixin).extend(b.mixin(g,{defaults:{state:a.NEW,deleted:false,purged:false,name:"(unnamed dataset)",accessible:true,data_type:"",file_ext:"",file_size:0,meta_files:[],misc_blurb:"",misc_info:"",tags:[]},initialize:function(h,i){this.debug(this+"(Dataset).initialize",h,i);if(!this.get("accessible")){this.set("state",a.NOT_VIEWABLE)}this.urls=this._generateUrls();this._setUpListeners()},_generateUrls:function(){var j=this.get("id");if(!j){return{}}var i={purge:"datasets/"+j+"/purge_async",display:"datasets/"+j+"/display/?preview=True",edit:"datasets/"+j+"/edit",download:"datasets/"+j+"/display?to_ext="+this.get("file_ext"),report_error:"dataset/errors?id="+j,rerun:"tool_runner/rerun?id="+j,show_params:"datasets/"+j+"/show_params",visualization:"visualization",meta_download:"dataset/get_metadata_file?hda_id="+j+"&metadata_name="};var h=(window.galaxy_config&&galaxy_config.root)?(galaxy_config.root):("/");_.each(i,function(l,k){i[k]=h+l});this.urls=i;return i},_setUpListeners:function(){this.on("change:state",function(i,h){this.log(this+" has changed state:",i,h);if(this.inReadyState()){this.trigger("state:ready",i,h,this.previous("state"))}});this.on("change:id change:file_ext",function(h){this._generateUrls()})},toJSON:function(){var h=Backbone.Model.prototype.toJSON.call(this);return _.extend(h,{urls:this.urls})},isDeletedOrPurged:function(){return(this.get("deleted")||this.get("purged"))},inReadyState:function(){var h=_.contains(a.READY_STATES,this.get("state"));return(this.isDeletedOrPurged()||h)},hasDetails:function(){return _.has(this.attributes,"genome_build")},hasData:function(){return(this.get("file_size")>0)},"delete":function(h){if(this.get("deleted")){return jQuery.when()}return this.save({deleted:true},h)},undelete:function(h){if(!this.get("deleted")||this.get("purged")){return jQuery.when()}return this.save({deleted:false},h)},purge:function e(h){if(this.get("purged")){return jQuery.when()}h=h||{};h.url=this.urls.purge;var i=this,j=jQuery.ajax(h);j.done(function(m,k,l){i.set({deleted:true,purged:true})});j.fail(function(o,k,n){var l=d("Unable to purge dataset");var m=("Removal of datasets by users is not allowed in this Galaxy instance");if(o.responseJSON&&o.responseJSON.error){l=o.responseJSON.error}else{if(o.responseText.indexOf(m)!==-1){l=m}}o.responseText=l;i.trigger("error",i,o,h,d(l),{error:l})});return j},searchAttributes:["name","file_ext","genome_build","misc_blurb","misc_info","annotation","tags"],searchAliases:{title:"name",format:"file_ext",database:"genome_build",blurb:"misc_blurb",description:"misc_blurb",info:"misc_info",tag:"tags"},toString:function(){var h=this.get("id")||"";if(this.get("name")){h='"'+this.get("name")+'",'+h}return"Dataset("+h+")"}}));var c=Backbone.Collection.extend(b.LoggableMixin).extend({model:f,urlRoot:((window.galaxy_config&&galaxy_config.root)?(galaxy_config.root):("/"))+"api/datasets",ids:function(){return this.map(function(h){return h.get("id")})},notReady:function(){return this.filter(function(h){return !h.inReadyState()})},haveDetails:function(){return this.all(function(h){return h.hasDetails()})},ajaxQueue:function(m,j){var i=jQuery.Deferred(),h=this.length,l=[];if(!h){i.resolve([]);return i}var k=this.chain().reverse().map(function(o,n){return function(){var p=m.call(o,j);p.done(function(q){i.notify({curr:n,total:h,response:q,model:o})});p.always(function(q){l.push(q);if(k.length){k.shift()()}else{i.resolve(l)}})}}).value();k.shift()();return i},matches:function(h){return this.filter(function(i){return i.matches(h)})},set:function(j,h){var i=this;j=_.map(j,function(l){if(!i.get(l.id)){return l}var k=existing.toJSON();_.extend(k,l);return k});Backbone.Collection.prototype.set.call(this,j,h)},toString:function(){return(["DatasetAssociationCollection(",this.length,")"].join(""))}});return{DatasetAssociation:f,DatasetAssociationCollection:c}});
File diff suppressed because one or more lines are too long