mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-10-25 00:12:08 +08:00
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:
@@ -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
|
||||
};
|
||||
});
|
||||
@@ -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
|
||||
};
|
||||
});
|
||||
@@ -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
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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
|
||||
};
|
||||
});
|
||||
@@ -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
Reference in New Issue
Block a user