PageRenderTime 62ms CodeModel.GetById 17ms RepoModel.GetById 1ms app.codeStats 0ms

/static/scripts/mvc/collection/paired-collection-creator.js

https://bitbucket.org/afgane/galaxy-central
JavaScript | 1719 lines | 1278 code | 168 blank | 273 comment | 139 complexity | 12cb613a302b1ed9ad82d795b1d3994a MD5 | raw file
Possible License(s): CC-BY-3.0
  1. define([
  2. "utils/levenshtein",
  3. "utils/natural-sort",
  4. "mvc/base-mvc",
  5. "utils/localization"
  6. ], function( levenshteinDistance, naturalSort, baseMVC, _l ){
  7. /* ============================================================================
  8. TODO:
  9. PROGRAMMATICALLY:
  10. currPanel.once( 'rendered', function(){
  11. currPanel.showSelectors();
  12. currPanel.selectAll();
  13. _.last( currPanel.actionsPopup.options ).func();
  14. });
  15. ============================================================================ */
  16. /** A view for paired datasets in the collections creator.
  17. */
  18. var PairView = Backbone.View.extend( baseMVC.LoggableMixin ).extend({
  19. tagName : 'li',
  20. className : 'dataset paired',
  21. initialize : function( attributes ){
  22. this.pair = attributes.pair || {};
  23. },
  24. template : _.template([
  25. '<span class="forward-dataset-name flex-column"><%= pair.forward.name %></span>',
  26. '<span class="pair-name-column flex-column">',
  27. '<span class="pair-name"><%= pair.name %></span>',
  28. '</span>',
  29. '<span class="reverse-dataset-name flex-column"><%= pair.reverse.name %></span>'
  30. ].join('')),
  31. render : function(){
  32. this.$el
  33. .attr( 'draggable', true )
  34. .data( 'pair', this.pair )
  35. .html( this.template({ pair: this.pair }) )
  36. .addClass( 'flex-column-container' );
  37. return this;
  38. },
  39. events : {
  40. 'dragstart' : '_dragstart',
  41. 'dragend' : '_dragend',
  42. 'dragover' : '_sendToParent',
  43. 'drop' : '_sendToParent'
  44. },
  45. /** dragging pairs for re-ordering */
  46. _dragstart : function( ev ){
  47. ev.currentTarget.style.opacity = '0.4';
  48. if( ev.originalEvent ){ ev = ev.originalEvent; }
  49. ev.dataTransfer.effectAllowed = 'move';
  50. ev.dataTransfer.setData( 'text/plain', JSON.stringify( this.pair ) );
  51. this.$el.parent().trigger( 'pair.dragstart', [ this ] );
  52. },
  53. /** dragging pairs for re-ordering */
  54. _dragend : function( ev ){
  55. ev.currentTarget.style.opacity = '1.0';
  56. this.$el.parent().trigger( 'pair.dragend', [ this ] );
  57. },
  58. /** manually bubble up an event to the parent/container */
  59. _sendToParent : function( ev ){
  60. this.$el.parent().trigger( ev );
  61. },
  62. /** string rep */
  63. toString : function(){
  64. return 'PairView(' + this.pair.name + ')';
  65. }
  66. });
  67. // ============================================================================
  68. /** returns an autopair function that uses the provided options.match function */
  69. function autoPairFnBuilder( options ){
  70. options = options || {};
  71. options.createPair = options.createPair || function _defaultCreatePair( params ){
  72. this.debug( 'creating pair:', params.listA[ params.indexA ].name, params.listB[ params.indexB ].name );
  73. params = params || {};
  74. return this._pair(
  75. params.listA.splice( params.indexA, 1 )[0],
  76. params.listB.splice( params.indexB, 1 )[0],
  77. { silent: true }
  78. );
  79. };
  80. // compile these here outside of the loop
  81. var _regexps = [];
  82. function getRegExps(){
  83. if( !_regexps.length ){
  84. _regexps = [
  85. new RegExp( this.filters[0] ),
  86. new RegExp( this.filters[1] )
  87. ];
  88. }
  89. return _regexps;
  90. }
  91. // mangle params as needed
  92. options.preprocessMatch = options.preprocessMatch || function _defaultPreprocessMatch( params ){
  93. var regexps = getRegExps.call( this );
  94. return _.extend( params, {
  95. matchTo : params.matchTo.name.replace( regexps[0], '' ),
  96. possible : params.possible.name.replace( regexps[1], '' )
  97. });
  98. };
  99. return function _strategy( params ){
  100. this.debug( 'autopair _strategy ---------------------------' );
  101. params = params || {};
  102. var listA = params.listA,
  103. listB = params.listB,
  104. indexA = 0, indexB,
  105. bestMatch = {
  106. score : 0.0,
  107. index : null
  108. },
  109. paired = [];
  110. //console.debug( 'params:', JSON.stringify( params, null, ' ' ) );
  111. this.debug( 'starting list lens:', listA.length, listB.length );
  112. this.debug( 'bestMatch (starting):', JSON.stringify( bestMatch, null, ' ' ) );
  113. while( indexA < listA.length ){
  114. var matchTo = listA[ indexA ];
  115. bestMatch.score = 0.0;
  116. for( indexB=0; indexB<listB.length; indexB++ ){
  117. var possible = listB[ indexB ];
  118. this.debug( indexA + ':' + matchTo.name );
  119. this.debug( indexB + ':' + possible.name );
  120. // no matching with self
  121. if( listA[ indexA ] !== listB[ indexB ] ){
  122. bestMatch = options.match.call( this, options.preprocessMatch.call( this, {
  123. matchTo : matchTo,
  124. possible: possible,
  125. index : indexB,
  126. bestMatch : bestMatch
  127. }));
  128. this.debug( 'bestMatch:', JSON.stringify( bestMatch, null, ' ' ) );
  129. if( bestMatch.score === 1.0 ){
  130. this.debug( 'breaking early due to perfect match' );
  131. break;
  132. }
  133. }
  134. }
  135. var scoreThreshold = options.scoreThreshold.call( this );
  136. this.debug( 'scoreThreshold:', scoreThreshold );
  137. this.debug( 'bestMatch.score:', bestMatch.score );
  138. if( bestMatch.score >= scoreThreshold ){
  139. this.debug( 'creating pair' );
  140. paired.push( options.createPair.call( this, {
  141. listA : listA,
  142. indexA : indexA,
  143. listB : listB,
  144. indexB : bestMatch.index
  145. }));
  146. this.debug( 'list lens now:', listA.length, listB.length );
  147. } else {
  148. indexA += 1;
  149. }
  150. if( !listA.length || !listB.length ){
  151. return paired;
  152. }
  153. }
  154. this.debug( 'paired:', JSON.stringify( paired, null, ' ' ) );
  155. this.debug( 'autopair _strategy ---------------------------' );
  156. return paired;
  157. };
  158. }
  159. // ============================================================================
  160. /** An interface for building collections of paired datasets.
  161. */
  162. var PairedCollectionCreator = Backbone.View.extend( baseMVC.LoggableMixin ).extend({
  163. className: 'collection-creator flex-row-container',
  164. /** set up initial options, instance vars, behaviors, and autopair (if set to do so) */
  165. initialize : function( attributes ){
  166. this.metric( 'PairedCollectionCreator.initialize', attributes );
  167. //this.debug( '-- PairedCollectionCreator:', attributes );
  168. attributes = _.defaults( attributes, {
  169. datasets : [],
  170. filters : this.DEFAULT_FILTERS,
  171. automaticallyPair : true,
  172. strategy : 'lcs',
  173. matchPercentage : 0.9,
  174. twoPassAutopairing : true
  175. });
  176. /** unordered, original list */
  177. this.initialList = attributes.datasets;
  178. /** is this from a history? if so, what's its id? */
  179. this.historyId = attributes.historyId;
  180. /** which filters should be used initially? (String[2] or name in commonFilters) */
  181. this.filters = this.commonFilters[ attributes.filters ] || this.commonFilters[ this.DEFAULT_FILTERS ];
  182. if( _.isArray( attributes.filters ) ){
  183. this.filters = attributes.filters;
  184. }
  185. /** try to auto pair the unpaired datasets on load? */
  186. this.automaticallyPair = attributes.automaticallyPair;
  187. /** what method to use for auto pairing (will be passed aggression level) */
  188. this.strategy = this.strategies[ attributes.strategy ] || this.strategies[ this.DEFAULT_STRATEGY ];
  189. if( _.isFunction( attributes.strategy ) ){
  190. this.strategy = attributes.strategy;
  191. }
  192. /** distance/mismatch level allowed for autopairing */
  193. this.matchPercentage = attributes.matchPercentage;
  194. /** try to autopair using simple first, then this.strategy on the remainder */
  195. this.twoPassAutopairing = attributes.twoPassAutopairing;
  196. /** remove file extensions (\.*) from created pair names? */
  197. this.removeExtensions = true;
  198. //this.removeExtensions = false;
  199. /** fn to call when the cancel button is clicked (scoped to this) - if falsy, no btn is displayed */
  200. this.oncancel = attributes.oncancel;
  201. /** fn to call when the collection is created (scoped to this) */
  202. this.oncreate = attributes.oncreate;
  203. /** fn to call when the cancel button is clicked (scoped to this) - if falsy, no btn is displayed */
  204. this.autoscrollDist = attributes.autoscrollDist || 24;
  205. /** is the unpaired panel shown? */
  206. this.unpairedPanelHidden = false;
  207. /** is the paired panel shown? */
  208. this.pairedPanelHidden = false;
  209. /** DOM elements currently being dragged */
  210. this.$dragging = null;
  211. this._setUpBehaviors();
  212. this._dataSetUp();
  213. },
  214. /** map of common filter pairs by name */
  215. commonFilters : {
  216. illumina : [ '_1', '_2' ],
  217. Rs : [ '_R1', '_R2' ]
  218. },
  219. /** which commonFilter to use by default */
  220. DEFAULT_FILTERS : 'illumina',
  221. /** map of name->fn for autopairing */
  222. strategies : {
  223. 'simple' : 'autopairSimple',
  224. 'lcs' : 'autopairLCS',
  225. 'levenshtein' : 'autopairLevenshtein'
  226. },
  227. /** default autopair strategy name */
  228. DEFAULT_STRATEGY : 'lcs',
  229. // ------------------------------------------------------------------------ process raw list
  230. /** set up main data: cache initialList, sort, and autopair */
  231. _dataSetUp : function(){
  232. //this.debug( '-- _dataSetUp' );
  233. this.paired = [];
  234. this.unpaired = [];
  235. this.selectedIds = [];
  236. // sort initial list, add ids if needed, and save new working copy to unpaired
  237. this._sortInitialList();
  238. this._ensureIds();
  239. this.unpaired = this.initialList.slice( 0 );
  240. if( this.automaticallyPair ){
  241. this.autoPair();
  242. this.once( 'rendered:initial', function(){
  243. this.trigger( 'autopair' );
  244. });
  245. }
  246. },
  247. /** sort initial list */
  248. _sortInitialList : function(){
  249. //this.debug( '-- _sortInitialList' );
  250. this._sortDatasetList( this.initialList );
  251. },
  252. /** sort a list of datasets */
  253. _sortDatasetList : function( list ){
  254. // currently only natural sort by name
  255. list.sort( function( a, b ){ return naturalSort( a.name, b.name ); });
  256. return list;
  257. },
  258. /** add ids to dataset objs in initial list if none */
  259. _ensureIds : function(){
  260. this.initialList.forEach( function( dataset ){
  261. if( !dataset.hasOwnProperty( 'id' ) ){
  262. dataset.id = _.uniqueId();
  263. }
  264. });
  265. return this.initialList;
  266. },
  267. /** split initial list into two lists, those that pass forward filters & those passing reverse */
  268. _splitByFilters : function(){
  269. var regexFilters = this.filters.map( function( stringFilter ){
  270. return new RegExp( stringFilter );
  271. }),
  272. split = [ [], [] ];
  273. function _filter( unpaired, filter ){
  274. return filter.test( unpaired.name );
  275. //return dataset.name.indexOf( filter ) >= 0;
  276. }
  277. this.unpaired.forEach( function _filterEach( unpaired ){
  278. // 90% of the time this seems to work, but:
  279. //TODO: this treats *all* strings as regex which may confuse people - possibly check for // surrounding?
  280. // would need explanation in help as well
  281. regexFilters.forEach( function( filter, i ){
  282. if( _filter( unpaired, filter ) ){
  283. split[i].push( unpaired );
  284. }
  285. });
  286. });
  287. return split;
  288. },
  289. /** add a dataset to the unpaired list in it's proper order */
  290. _addToUnpaired : function( dataset ){
  291. // currently, unpaired is natural sorted by name, use binary search to find insertion point
  292. var binSearchSortedIndex = function( low, hi ){
  293. if( low === hi ){ return low; }
  294. var mid = Math.floor( ( hi - low ) / 2 ) + low,
  295. compared = naturalSort( dataset.name, this.unpaired[ mid ].name );
  296. if( compared < 0 ){
  297. return binSearchSortedIndex( low, mid );
  298. } else if( compared > 0 ){
  299. return binSearchSortedIndex( mid + 1, hi );
  300. }
  301. // walk the equal to find the last
  302. while( this.unpaired[ mid ] && this.unpaired[ mid ].name === dataset.name ){ mid++; }
  303. return mid;
  304. }.bind( this );
  305. this.unpaired.splice( binSearchSortedIndex( 0, this.unpaired.length ), 0, dataset );
  306. },
  307. // ------------------------------------------------------------------------ auto pairing
  308. /** two passes to automatically create pairs:
  309. * use both simpleAutoPair, then the fn mentioned in strategy
  310. */
  311. autoPair : function( strategy ){
  312. // split first using exact matching
  313. var split = this._splitByFilters(),
  314. paired = [];
  315. if( this.twoPassAutopairing ){
  316. paired = this.autopairSimple({
  317. listA : split[0],
  318. listB : split[1]
  319. });
  320. split = this._splitByFilters();
  321. }
  322. // uncomment to see printlns while running tests
  323. //this.debug = function(){ console.log.apply( console, arguments ); };
  324. // then try the remainder with something less strict
  325. strategy = strategy || this.strategy;
  326. split = this._splitByFilters();
  327. paired = paired.concat( this[ strategy ].call( this, {
  328. listA : split[0],
  329. listB : split[1]
  330. }));
  331. return paired;
  332. },
  333. /** autopair by exact match */
  334. autopairSimple : autoPairFnBuilder({
  335. scoreThreshold: function(){ return 1.0; },
  336. match : function _match( params ){
  337. params = params || {};
  338. if( params.matchTo === params.possible ){
  339. return {
  340. index: params.index,
  341. score: 1.0
  342. };
  343. }
  344. return params.bestMatch;
  345. }
  346. }),
  347. /** autopair by levenshtein edit distance scoring */
  348. autopairLevenshtein : autoPairFnBuilder({
  349. scoreThreshold: function(){ return this.matchPercentage; },
  350. match : function _matches( params ){
  351. params = params || {};
  352. var distance = levenshteinDistance( params.matchTo, params.possible ),
  353. score = 1.0 - ( distance / ( Math.max( params.matchTo.length, params.possible.length ) ) );
  354. if( score > params.bestMatch.score ){
  355. return {
  356. index: params.index,
  357. score: score
  358. };
  359. }
  360. return params.bestMatch;
  361. }
  362. }),
  363. /** autopair by longest common substrings scoring */
  364. autopairLCS : autoPairFnBuilder({
  365. scoreThreshold: function(){ return this.matchPercentage; },
  366. match : function _matches( params ){
  367. params = params || {};
  368. var match = this._naiveStartingAndEndingLCS( params.matchTo, params.possible ).length,
  369. score = match / ( Math.max( params.matchTo.length, params.possible.length ) );
  370. if( score > params.bestMatch.score ){
  371. return {
  372. index: params.index,
  373. score: score
  374. };
  375. }
  376. return params.bestMatch;
  377. }
  378. }),
  379. /** return the concat'd longest common prefix and suffix from two strings */
  380. _naiveStartingAndEndingLCS : function( s1, s2 ){
  381. var fwdLCS = '',
  382. revLCS = '',
  383. i = 0, j = 0;
  384. while( i < s1.length && i < s2.length ){
  385. if( s1[ i ] !== s2[ i ] ){
  386. break;
  387. }
  388. fwdLCS += s1[ i ];
  389. i += 1;
  390. }
  391. if( i === s1.length ){ return s1; }
  392. if( i === s2.length ){ return s2; }
  393. i = ( s1.length - 1 );
  394. j = ( s2.length - 1 );
  395. while( i >= 0 && j >= 0 ){
  396. if( s1[ i ] !== s2[ j ] ){
  397. break;
  398. }
  399. revLCS = [ s1[ i ], revLCS ].join( '' );
  400. i -= 1;
  401. j -= 1;
  402. }
  403. return fwdLCS + revLCS;
  404. },
  405. // ------------------------------------------------------------------------ pairing / unpairing
  406. /** create a pair from fwd and rev, removing them from unpaired, and placing the new pair in paired */
  407. _pair : function( fwd, rev, options ){
  408. options = options || {};
  409. //this.debug( '_pair:', fwd, rev );
  410. var pair = this._createPair( fwd, rev, options.name );
  411. this.paired.push( pair );
  412. this.unpaired = _.without( this.unpaired, fwd, rev );
  413. if( !options.silent ){
  414. this.trigger( 'pair:new', pair );
  415. }
  416. return pair;
  417. },
  418. /** create a pair Object from fwd and rev, adding the name attribute (will guess if not given) */
  419. _createPair : function( fwd, rev, name ){
  420. // ensure existance and don't pair something with itself
  421. if( !( fwd && rev ) || ( fwd === rev ) ){
  422. throw new Error( 'Bad pairing: ' + [ JSON.stringify( fwd ), JSON.stringify( rev ) ] );
  423. }
  424. name = name || this._guessNameForPair( fwd, rev );
  425. return { forward : fwd, name : name, reverse : rev };
  426. },
  427. /** try to find a good pair name for the given fwd and rev datasets */
  428. _guessNameForPair : function( fwd, rev, removeExtensions ){
  429. removeExtensions = ( removeExtensions !== undefined )?( removeExtensions ):( this.removeExtensions );
  430. var fwdName = fwd.name,
  431. revName = rev.name,
  432. lcs = this._naiveStartingAndEndingLCS(
  433. fwdName.replace( this.filters[0], '' ),
  434. revName.replace( this.filters[1], '' )
  435. );
  436. if( removeExtensions ){
  437. var lastDotIndex = lcs.lastIndexOf( '.' );
  438. if( lastDotIndex > 0 ){
  439. var extension = lcs.slice( lastDotIndex, lcs.length );
  440. lcs = lcs.replace( extension, '' );
  441. fwdName = fwdName.replace( extension, '' );
  442. revName = revName.replace( extension, '' );
  443. }
  444. }
  445. return lcs || ( fwdName + ' & ' + revName );
  446. },
  447. /** unpair a pair, removing it from paired, and adding the fwd,rev datasets back into unpaired */
  448. _unpair : function( pair, options ){
  449. options = options || {};
  450. if( !pair ){
  451. throw new Error( 'Bad pair: ' + JSON.stringify( pair ) );
  452. }
  453. this.paired = _.without( this.paired, pair );
  454. this._addToUnpaired( pair.forward );
  455. this._addToUnpaired( pair.reverse );
  456. if( !options.silent ){
  457. this.trigger( 'pair:unpair', [ pair ] );
  458. }
  459. return pair;
  460. },
  461. /** unpair all paired datasets */
  462. unpairAll : function(){
  463. var pairs = [];
  464. while( this.paired.length ){
  465. pairs.push( this._unpair( this.paired[ 0 ], { silent: true }) );
  466. }
  467. this.trigger( 'pair:unpair', pairs );
  468. },
  469. // ------------------------------------------------------------------------ API
  470. /** convert a pair into JSON compatible with the collections API */
  471. _pairToJSON : function( pair, src ){
  472. src = src || 'hda';
  473. //TODO: consider making this the pair structure when created instead
  474. return {
  475. collection_type : 'paired',
  476. src : 'new_collection',
  477. name : pair.name,
  478. element_identifiers : [{
  479. name : 'forward',
  480. id : pair.forward.id,
  481. src : src
  482. }, {
  483. name : 'reverse',
  484. id : pair.reverse.id,
  485. src : src
  486. }]
  487. };
  488. },
  489. /** create the collection via the API
  490. * @returns {jQuery.xhr Object} the jquery ajax request
  491. */
  492. createList : function( name ){
  493. var creator = this,
  494. url = '/api/histories/' + this.historyId + '/contents/dataset_collections';
  495. //TODO: use ListPairedCollection.create()
  496. var ajaxData = {
  497. type : 'dataset_collection',
  498. collection_type : 'list:paired',
  499. name : _.escape( name || creator.$( '.collection-name' ).val() ),
  500. element_identifiers : creator.paired.map( function( pair ){
  501. return creator._pairToJSON( pair );
  502. })
  503. };
  504. //this.debug( JSON.stringify( ajaxData ) );
  505. return jQuery.ajax( url, {
  506. type : 'POST',
  507. contentType : 'application/json',
  508. dataType : 'json',
  509. data : JSON.stringify( ajaxData )
  510. })
  511. .fail( function( xhr, status, message ){
  512. creator._ajaxErrHandler( xhr, status, message );
  513. })
  514. .done( function( response, message, xhr ){
  515. //this.info( 'ok', response, message, xhr );
  516. creator.trigger( 'collection:created', response, message, xhr );
  517. creator.metric( 'collection:created', response );
  518. if( typeof creator.oncreate === 'function' ){
  519. creator.oncreate.call( this, response, message, xhr );
  520. }
  521. });
  522. },
  523. /** handle ajax errors with feedback and details to the user (if available) */
  524. _ajaxErrHandler : function( xhr, status, message ){
  525. this.error( xhr, status, message );
  526. var content = _l( 'An error occurred while creating this collection' );
  527. if( xhr ){
  528. if( xhr.readyState === 0 && xhr.status === 0 ){
  529. content += ': ' + _l( 'Galaxy could not be reached and may be updating.' )
  530. + _l( ' Try again in a few minutes.' );
  531. } else if( xhr.responseJSON ){
  532. content += '<br /><pre>' + JSON.stringify( xhr.responseJSON ) + '</pre>';
  533. } else {
  534. content += ': ' + message;
  535. }
  536. }
  537. creator._showAlert( content, 'alert-danger' );
  538. },
  539. // ------------------------------------------------------------------------ rendering
  540. /** render the entire interface */
  541. render : function( speed, callback ){
  542. //this.debug( '-- _render' );
  543. //this.$el.empty().html( PairedCollectionCreator.templates.main() );
  544. this.$el.empty().html( PairedCollectionCreator.templates.main() );
  545. this._renderHeader( speed );
  546. this._renderMiddle( speed );
  547. this._renderFooter( speed );
  548. this._addPluginComponents();
  549. this.trigger( 'rendered', this );
  550. return this;
  551. },
  552. /** render the header section */
  553. _renderHeader : function( speed, callback ){
  554. //this.debug( '-- _renderHeader' );
  555. var $header = this.$( '.header' ).empty().html( PairedCollectionCreator.templates.header() )
  556. .find( '.help-content' ).prepend( $( PairedCollectionCreator.templates.helpContent() ) );
  557. this._renderFilters();
  558. return $header;
  559. },
  560. /** fill the filter inputs with the filter values */
  561. _renderFilters : function(){
  562. return this.$( '.forward-column .column-header input' ).val( this.filters[0] )
  563. .add( this.$( '.reverse-column .column-header input' ).val( this.filters[1] ) );
  564. },
  565. /** render the middle including unpaired and paired sections (which may be hidden) */
  566. _renderMiddle : function( speed, callback ){
  567. var $middle = this.$( '.middle' ).empty().html( PairedCollectionCreator.templates.middle() );
  568. // (re-) hide the un/paired panels based on instance vars
  569. if( this.unpairedPanelHidden ){
  570. this.$( '.unpaired-columns' ).hide();
  571. } else if( this.pairedPanelHidden ){
  572. this.$( '.paired-columns' ).hide();
  573. }
  574. this._renderUnpaired();
  575. this._renderPaired();
  576. return $middle;
  577. },
  578. /** render the unpaired section, showing datasets accrd. to filters, update the unpaired counts */
  579. _renderUnpaired : function( speed, callback ){
  580. //this.debug( '-- _renderUnpaired' );
  581. var creator = this,
  582. $fwd, $rev, $prd = [],
  583. split = this._splitByFilters();
  584. // update unpaired counts
  585. this.$( '.forward-column .title' )
  586. .text([ split[0].length, _l( 'unpaired forward' ) ].join( ' ' ));
  587. this.$( '.forward-column .unpaired-info' )
  588. .text( this._renderUnpairedDisplayStr( this.unpaired.length - split[0].length ) );
  589. this.$( '.reverse-column .title' )
  590. .text([ split[1].length, _l( 'unpaired reverse' ) ].join( ' ' ));
  591. this.$( '.reverse-column .unpaired-info' )
  592. .text( this._renderUnpairedDisplayStr( this.unpaired.length - split[1].length ) );
  593. this.$( '.unpaired-columns .column-datasets' ).empty();
  594. // show/hide the auto pair button if any unpaired are left
  595. this.$( '.autopair-link' ).toggle( this.unpaired.length !== 0 );
  596. if( this.unpaired.length === 0 ){
  597. this._renderUnpairedEmpty();
  598. return;
  599. }
  600. // create the dataset dom arrays
  601. $rev = split[1].map( function( dataset, i ){
  602. // if there'll be a fwd dataset across the way, add a button to pair the row
  603. if( ( split[0][ i ] !== undefined )
  604. && ( split[0][ i ] !== dataset ) ){
  605. $prd.push( creator._renderPairButton() );
  606. }
  607. return creator._renderUnpairedDataset( dataset );
  608. });
  609. $fwd = split[0].map( function( dataset ){
  610. return creator._renderUnpairedDataset( dataset );
  611. });
  612. if( !$fwd.length && !$rev.length ){
  613. this._renderUnpairedNotShown();
  614. return;
  615. }
  616. // add to appropo cols
  617. //TODO: not the best way to render - consider rendering the entire unpaired-columns section in a fragment
  618. // and swapping out that
  619. this.$( '.unpaired-columns .forward-column .column-datasets' ).append( $fwd )
  620. .add( this.$( '.unpaired-columns .paired-column .column-datasets' ).append( $prd ) )
  621. .add( this.$( '.unpaired-columns .reverse-column .column-datasets' ).append( $rev ) );
  622. this._adjUnpairedOnScrollbar();
  623. },
  624. /** return a string to display the count of filtered out datasets */
  625. _renderUnpairedDisplayStr : function( numFiltered ){
  626. return [ '(', numFiltered, ' ', _l( 'filtered out' ), ')' ].join('');
  627. },
  628. /** return an unattached jQuery DOM element to represent an unpaired dataset */
  629. _renderUnpairedDataset : function( dataset ){
  630. //TODO: to underscore template
  631. return $( '<li/>')
  632. .attr( 'id', 'dataset-' + dataset.id )
  633. .addClass( 'dataset unpaired' )
  634. .attr( 'draggable', true )
  635. .addClass( dataset.selected? 'selected': '' )
  636. .append( $( '<span/>' ).addClass( 'dataset-name' ).text( dataset.name ) )
  637. //??
  638. .data( 'dataset', dataset );
  639. },
  640. /** render the button that may go between unpaired datasets, allowing the user to pair a row */
  641. _renderPairButton : function(){
  642. //TODO: *not* a dataset - don't pretend like it is
  643. return $( '<li/>').addClass( 'dataset unpaired' )
  644. .append( $( '<span/>' ).addClass( 'dataset-name' ).text( _l( 'Pair these datasets' ) ) );
  645. },
  646. /** a message to display when no unpaired left */
  647. _renderUnpairedEmpty : function(){
  648. //this.debug( '-- renderUnpairedEmpty' );
  649. var $msg = $( '<div class="empty-message"></div>' )
  650. .text( '(' + _l( 'no remaining unpaired datasets' ) + ')' );
  651. this.$( '.unpaired-columns .paired-column .column-datasets' ).empty().prepend( $msg );
  652. return $msg;
  653. },
  654. /** a message to display when no unpaired can be shown with the current filters */
  655. _renderUnpairedNotShown : function(){
  656. //this.debug( '-- renderUnpairedEmpty' );
  657. var $msg = $( '<div class="empty-message"></div>' )
  658. .text( '(' + _l( 'no datasets were found matching the current filters' ) + ')' );
  659. this.$( '.unpaired-columns .paired-column .column-datasets' ).empty().prepend( $msg );
  660. return $msg;
  661. },
  662. /** try to detect if the unpaired section has a scrollbar and adjust left column for better centering of all */
  663. _adjUnpairedOnScrollbar : function(){
  664. var $unpairedColumns = this.$( '.unpaired-columns' ).last(),
  665. $firstDataset = this.$( '.unpaired-columns .reverse-column .dataset' ).first();
  666. if( !$firstDataset.size() ){ return; }
  667. var ucRight = $unpairedColumns.offset().left + $unpairedColumns.outerWidth(),
  668. dsRight = $firstDataset.offset().left + $firstDataset.outerWidth(),
  669. rightDiff = Math.floor( ucRight ) - Math.floor( dsRight );
  670. //this.debug( 'rightDiff:', ucRight, '-', dsRight, '=', rightDiff );
  671. this.$( '.unpaired-columns .forward-column' )
  672. .css( 'margin-left', ( rightDiff > 0 )? rightDiff: 0 );
  673. },
  674. /** render the paired section and update counts of paired datasets */
  675. _renderPaired : function( speed, callback ){
  676. //this.debug( '-- _renderPaired' );
  677. this.$( '.paired-column-title .title' ).text([ this.paired.length, _l( 'paired' ) ].join( ' ' ) );
  678. // show/hide the unpair all link
  679. this.$( '.unpair-all-link' ).toggle( this.paired.length !== 0 );
  680. if( this.paired.length === 0 ){
  681. this._renderPairedEmpty();
  682. return;
  683. //TODO: would be best to return here (the $columns)
  684. } else {
  685. // show/hide 'remove extensions link' when any paired and they seem to have extensions
  686. this.$( '.remove-extensions-link' ).show();
  687. }
  688. this.$( '.paired-columns .column-datasets' ).empty();
  689. var creator = this;
  690. this.paired.forEach( function( pair, i ){
  691. //TODO: cache these?
  692. var pairView = new PairView({ pair: pair });
  693. creator.$( '.paired-columns .column-datasets' )
  694. .append( pairView.render().$el )
  695. .append([
  696. '<button class="unpair-btn">',
  697. '<span class="fa fa-unlink" title="', _l( 'Unpair' ), '"></span>',
  698. '</button>'
  699. ].join( '' ));
  700. });
  701. },
  702. /** a message to display when none paired */
  703. _renderPairedEmpty : function(){
  704. var $msg = $( '<div class="empty-message"></div>' )
  705. .text( '(' + _l( 'no paired datasets yet' ) + ')' );
  706. this.$( '.paired-columns .column-datasets' ).empty().prepend( $msg );
  707. return $msg;
  708. },
  709. /** render the footer, completion controls, and cancel controls */
  710. _renderFooter : function( speed, callback ){
  711. var $footer = this.$( '.footer' ).empty().html( PairedCollectionCreator.templates.footer() );
  712. this.$( '.remove-extensions' ).prop( 'checked', this.removeExtensions );
  713. if( typeof this.oncancel === 'function' ){
  714. this.$( '.cancel-create.btn' ).show();
  715. }
  716. return $footer;
  717. },
  718. /** add any jQuery/bootstrap/custom plugins to elements rendered */
  719. _addPluginComponents : function(){
  720. this._chooseFiltersPopover( '.choose-filters-link' );
  721. this.$( '.help-content i' ).hoverhighlight( '.collection-creator', 'rgba( 64, 255, 255, 1.0 )' );
  722. },
  723. /** build a filter selection popover allowing selection of common filter pairs */
  724. _chooseFiltersPopover : function( selector ){
  725. function filterChoice( val1, val2 ){
  726. return [
  727. '<button class="filter-choice btn" ',
  728. 'data-forward="', val1, '" data-reverse="', val2, '">',
  729. _l( 'Forward' ), ': ', val1, ', ',
  730. _l( 'Reverse' ), ': ', val2,
  731. '</button>'
  732. ].join('');
  733. }
  734. var $popoverContent = $( _.template([
  735. '<div class="choose-filters">',
  736. '<div class="help">',
  737. _l( 'Choose from the following filters to change which unpaired reads are shown in the display' ),
  738. ':</div>',
  739. _.values( this.commonFilters ).map( function( filterSet ){
  740. return filterChoice( filterSet[0], filterSet[1] );
  741. }).join( '' ),
  742. '</div>'
  743. ].join(''))({}));
  744. return this.$( selector ).popover({
  745. container : '.collection-creator',
  746. placement : 'bottom',
  747. html : true,
  748. //animation : false,
  749. content : $popoverContent
  750. });
  751. },
  752. /** add (or clear if clear is truthy) a validation warning to what */
  753. _validationWarning : function( what, clear ){
  754. var VALIDATION_CLASS = 'validation-warning';
  755. if( what === 'name' ){
  756. what = this.$( '.collection-name' ).add( this.$( '.collection-name-prompt' ) );
  757. this.$( '.collection-name' ).focus().select();
  758. }
  759. if( clear ){
  760. what = what || this.$( '.' + VALIDATION_CLASS );
  761. what.removeClass( VALIDATION_CLASS );
  762. } else {
  763. what.addClass( VALIDATION_CLASS );
  764. }
  765. },
  766. // ------------------------------------------------------------------------ events
  767. /** set up event handlers on self */
  768. _setUpBehaviors : function(){
  769. this.once( 'rendered', function(){
  770. this.trigger( 'rendered:initial', this );
  771. });
  772. this.on( 'pair:new', function(){
  773. //TODO: ideally only re-render the columns (or even elements) involved
  774. this._renderUnpaired();
  775. this._renderPaired();
  776. // scroll to bottom where new pairs are added
  777. //TODO: this doesn't seem to work - innerHeight sticks at 133...
  778. // may have to do with improper flex columns
  779. //var $pairedView = this.$( '.paired-columns' );
  780. //$pairedView.scrollTop( $pairedView.innerHeight() );
  781. //this.debug( $pairedView.height() )
  782. this.$( '.paired-columns' ).scrollTop( 8000000 );
  783. });
  784. this.on( 'pair:unpair', function( pairs ){
  785. //TODO: ideally only re-render the columns (or even elements) involved
  786. this._renderUnpaired();
  787. this._renderPaired();
  788. this.splitView();
  789. });
  790. this.on( 'filter-change', function(){
  791. this.filters = [
  792. this.$( '.forward-unpaired-filter input' ).val(),
  793. this.$( '.reverse-unpaired-filter input' ).val()
  794. ];
  795. this.metric( 'filter-change', this.filters );
  796. this._renderFilters();
  797. this._renderUnpaired();
  798. });
  799. this.on( 'autopair', function(){
  800. this._renderUnpaired();
  801. this._renderPaired();
  802. var message, msgClass = null;
  803. if( this.paired.length ){
  804. msgClass = 'alert-success';
  805. message = this.paired.length + ' ' + _l( 'pairs created' );
  806. if( !this.unpaired.length ){
  807. message += ': ' + _l( 'all datasets have been successfully paired' );
  808. this.hideUnpaired();
  809. this.$( '.collection-name' ).focus();
  810. }
  811. } else {
  812. message = _l( 'Could not automatically create any pairs from the given dataset names' );
  813. }
  814. this._showAlert( message, msgClass );
  815. });
  816. //this.on( 'all', function(){
  817. // this.info( arguments );
  818. //});
  819. return this;
  820. },
  821. events : {
  822. // header
  823. 'click .more-help' : '_clickMoreHelp',
  824. 'click .less-help' : '_clickLessHelp',
  825. 'click .header .alert button' : '_hideAlert',
  826. 'click .forward-column .column-title' : '_clickShowOnlyUnpaired',
  827. 'click .reverse-column .column-title' : '_clickShowOnlyUnpaired',
  828. 'click .unpair-all-link' : '_clickUnpairAll',
  829. //TODO: this seems kinda backasswards - re-sending jq event as a backbone event, can we listen directly?
  830. 'change .forward-unpaired-filter input' : function( ev ){ this.trigger( 'filter-change' ); },
  831. 'focus .forward-unpaired-filter input' : function( ev ){ $( ev.currentTarget ).select(); },
  832. 'click .autopair-link' : '_clickAutopair',
  833. 'click .choose-filters .filter-choice' : '_clickFilterChoice',
  834. 'click .clear-filters-link' : '_clearFilters',
  835. 'change .reverse-unpaired-filter input' : function( ev ){ this.trigger( 'filter-change' ); },
  836. 'focus .reverse-unpaired-filter input' : function( ev ){ $( ev.currentTarget ).select(); },
  837. // unpaired
  838. 'click .forward-column .dataset.unpaired' : '_clickUnpairedDataset',
  839. 'click .reverse-column .dataset.unpaired' : '_clickUnpairedDataset',
  840. 'click .paired-column .dataset.unpaired' : '_clickPairRow',
  841. 'click .unpaired-columns' : 'clearSelectedUnpaired',
  842. 'mousedown .unpaired-columns .dataset' : '_mousedownUnpaired',
  843. // divider
  844. 'click .paired-column-title' : '_clickShowOnlyPaired',
  845. 'mousedown .flexible-partition-drag' : '_startPartitionDrag',
  846. // paired
  847. 'click .paired-columns .dataset.paired' : 'selectPair',
  848. 'click .paired-columns' : 'clearSelectedPaired',
  849. 'click .paired-columns .pair-name' : '_clickPairName',
  850. 'click .unpair-btn' : '_clickUnpair',
  851. // paired - drop target
  852. //'dragenter .paired-columns' : '_dragenterPairedColumns',
  853. //'dragleave .paired-columns .column-datasets': '_dragleavePairedColumns',
  854. 'dragover .paired-columns .column-datasets' : '_dragoverPairedColumns',
  855. 'drop .paired-columns .column-datasets' : '_dropPairedColumns',
  856. 'pair.dragstart .paired-columns .column-datasets' : '_pairDragstart',
  857. 'pair.dragend .paired-columns .column-datasets' : '_pairDragend',
  858. // footer
  859. 'change .remove-extensions' : function( ev ){ this.toggleExtensions(); },
  860. 'change .collection-name' : '_changeName',
  861. 'keydown .collection-name' : '_nameCheckForEnter',
  862. 'click .cancel-create' : function( ev ){
  863. if( typeof this.oncancel === 'function' ){
  864. this.oncancel.call( this );
  865. }
  866. },
  867. 'click .create-collection' : '_clickCreate'//,
  868. },
  869. // ........................................................................ header
  870. /** expand help */
  871. _clickMoreHelp : function( ev ){
  872. this.$( '.main-help' ).addClass( 'expanded' );
  873. this.$( '.more-help' ).hide();
  874. },
  875. /** collapse help */
  876. _clickLessHelp : function( ev ){
  877. this.$( '.main-help' ).removeClass( 'expanded' );
  878. this.$( '.more-help' ).show();
  879. },
  880. /** show an alert on the top of the interface containing message (alertClass is bootstrap's alert-*)*/
  881. _showAlert : function( message, alertClass ){
  882. alertClass = alertClass || 'alert-danger';
  883. this.$( '.main-help' ).hide();
  884. this.$( '.header .alert' ).attr( 'class', 'alert alert-dismissable' ).addClass( alertClass ).show()
  885. .find( '.alert-message' ).html( message );
  886. },
  887. /** hide the alerts at the top */
  888. _hideAlert : function( message ){
  889. this.$( '.main-help' ).show();
  890. this.$( '.header .alert' ).hide();
  891. },
  892. /** toggle between showing only unpaired and split view */
  893. _clickShowOnlyUnpaired : function( ev ){
  894. //this.debug( 'click unpaired', ev.currentTarget );
  895. if( this.$( '.paired-columns' ).is( ':visible' ) ){
  896. this.hidePaired();
  897. } else {
  898. this.splitView();
  899. }
  900. },
  901. /** toggle between showing only paired and split view */
  902. _clickShowOnlyPaired : function( ev ){
  903. //this.debug( 'click paired' );
  904. if( this.$( '.unpaired-columns' ).is( ':visible' ) ){
  905. this.hideUnpaired();
  906. } else {
  907. this.splitView();
  908. }
  909. },
  910. /** hide unpaired, show paired */
  911. hideUnpaired : function( speed, callback ){
  912. this.unpairedPanelHidden = true;
  913. this.pairedPanelHidden = false;
  914. this._renderMiddle( speed, callback );
  915. },
  916. /** hide paired, show unpaired */
  917. hidePaired : function( speed, callback ){
  918. this.unpairedPanelHidden = false;
  919. this.pairedPanelHidden = true;
  920. this._renderMiddle( speed, callback );
  921. },
  922. /** show both paired and unpaired (splitting evenly) */
  923. splitView : function( speed, callback ){
  924. this.unpairedPanelHidden = this.pairedPanelHidden = false;
  925. this._renderMiddle( speed, callback );
  926. return this;
  927. },
  928. /** unpair all paired and do other super neat stuff which I'm not really sure about yet... */
  929. _clickUnpairAll : function( ev ){
  930. this.metric( 'unpairAll' );
  931. this.unpairAll();
  932. },
  933. /** attempt to autopair */
  934. _clickAutopair : function( ev ){
  935. var paired = this.autoPair();
  936. this.metric( 'autopair', paired.length, this.unpaired.length );
  937. this.trigger( 'autopair' );
  938. },
  939. /** set the filters based on the data attributes of the button click target */
  940. _clickFilterChoice : function( ev ){
  941. var $selected = $( ev.currentTarget );
  942. this.$( '.forward-unpaired-filter input' ).val( $selected.data( 'forward' ) );
  943. this.$( '.reverse-unpaired-filter input' ).val( $selected.data( 'reverse' ) );
  944. this._hideChooseFilters();
  945. this.trigger( 'filter-change' );
  946. },
  947. /** hide the choose filters popover */
  948. _hideChooseFilters : function(){
  949. //TODO: update bootstrap and remove the following hack
  950. // see also: https://github.com/twbs/bootstrap/issues/10260
  951. this.$( '.choose-filters-link' ).popover( 'hide' );
  952. this.$( '.popover' ).css( 'display', 'none' );
  953. },
  954. /** clear both filters */
  955. _clearFilters : function( ev ){
  956. this.$( '.forward-unpaired-filter input' ).val( '' );
  957. this.$( '.reverse-unpaired-filter input' ).val( '' );
  958. this.trigger( 'filter-change' );
  959. },
  960. // ........................................................................ unpaired
  961. /** select an unpaired dataset */
  962. _clickUnpairedDataset : function( ev ){
  963. ev.stopPropagation();
  964. return this.toggleSelectUnpaired( $( ev.currentTarget ) );
  965. },
  966. /** Toggle the selection of an unpaired dataset representation.
  967. * @param [jQuery] $dataset the unpaired dataset dom rep to select
  968. * @param [Boolean] options.force if defined, force selection based on T/F; otherwise, toggle
  969. */
  970. toggleSelectUnpaired : function( $dataset, options ){
  971. options = options || {};
  972. var dataset = $dataset.data( 'dataset' ),
  973. select = options.force !== undefined? options.force: !$dataset.hasClass( 'selected' );
  974. //this.debug( id, options.force, $dataset, dataset );
  975. if( !$dataset.size() || dataset === undefined ){ return $dataset; }
  976. if( select ){
  977. $dataset.addClass( 'selected' );
  978. if( !options.waitToPair ){
  979. this.pairAllSelected();
  980. }
  981. } else {
  982. $dataset.removeClass( 'selected' );
  983. //delete dataset.selected;
  984. }
  985. return $dataset;
  986. },
  987. /** pair all the currently selected unpaired datasets */
  988. pairAllSelected : function( options ){
  989. options = options || {};
  990. var creator = this,
  991. fwds = [],
  992. revs = [],
  993. pairs = [];
  994. creator.$( '.unpaired-columns .forward-column .dataset.selected' ).each( function(){
  995. fwds.push( $( this ).data( 'dataset' ) );
  996. });
  997. creator.$( '.unpaired-columns .reverse-column .dataset.selected' ).each( function(){
  998. revs.push( $( this ).data( 'dataset' ) );
  999. });
  1000. fwds.length = revs.length = Math.min( fwds.length, revs.length );
  1001. //this.debug( fwds );
  1002. //this.debug( revs );
  1003. fwds.forEach( function( fwd, i ){
  1004. try {
  1005. pairs.push( creator._pair( fwd, revs[i], { silent: true }) );
  1006. } catch( err ){
  1007. //TODO: preserve selected state of those that couldn't be paired
  1008. //TODO: warn that some could not be paired
  1009. creator.error( err );
  1010. }
  1011. });
  1012. if( pairs.length && !options.silent ){
  1013. this.trigger( 'pair:new', pairs );
  1014. }
  1015. return pairs;
  1016. },
  1017. /** clear the selection on all unpaired datasets */
  1018. clearSelectedUnpaired : function(){
  1019. this.$( '.unpaired-columns .dataset.selected' ).removeClass( 'selected' );
  1020. },
  1021. /** when holding down the shift key on a click, 'paint' the moused over datasets as selected */
  1022. _mousedownUnpaired : function( ev ){
  1023. if( ev.shiftKey ){
  1024. var creator = this,
  1025. $startTarget = $( ev.target ).addClass( 'selected' ),
  1026. moveListener = function( ev ){
  1027. creator.$( ev.target ).filter( '.dataset' ).addClass( 'selected' );
  1028. };
  1029. $startTarget.parent().on( 'mousemove', moveListener );
  1030. // on any mouseup, stop listening to the move and try to pair any selected
  1031. $( document ).one( 'mouseup', function( ev ){
  1032. $startTarget.parent().off( 'mousemove', moveListener );
  1033. creator.pairAllSelected();
  1034. });
  1035. }
  1036. },
  1037. /** attempt to pair two datasets directly across from one another */
  1038. _clickPairRow : function( ev ){
  1039. //if( !ev.currentTarget ){ return true; }
  1040. var rowIndex = $( ev.currentTarget ).index(),
  1041. fwd = $( '.unpaired-columns .forward-column .dataset' ).eq( rowIndex ).data( 'dataset' ),
  1042. rev = $( '.unpaired-columns .reverse-column .dataset' ).eq( rowIndex ).data( 'dataset' );
  1043. //this.debug( 'row:', rowIndex, fwd, rev );
  1044. this._pair( fwd, rev );
  1045. },
  1046. // ........................................................................ divider/partition
  1047. /** start dragging the visible divider/partition between unpaired and paired panes */
  1048. _startPartitionDrag : function( ev ){
  1049. var creator = this,
  1050. startingY = ev.pageY;
  1051. //this.debug( 'partition drag START:', ev );
  1052. $( 'body' ).css( 'cursor', 'ns-resize' );
  1053. creator.$( '.flexible-partition-drag' ).css( 'color', 'black' );
  1054. function endDrag( ev ){
  1055. //creator.debug( 'partition drag STOP:', ev );
  1056. // doing this by an added class didn't really work well - kept flashing still
  1057. creator.$( '.flexible-partition-drag' ).css( 'color', '' );
  1058. $( 'body' ).css( 'cursor', '' ).unbind( 'mousemove', trackMouse );
  1059. }
  1060. function trackMouse( ev ){
  1061. var offset = ev.pageY - startingY;
  1062. //creator.debug( 'partition:', startingY, offset );
  1063. if( !creator.adjPartition( offset ) ){
  1064. //creator.debug( 'mouseup triggered' );
  1065. $( 'body' ).trigger( 'mouseup' );
  1066. }
  1067. creator._adjUnpairedOnScrollbar();
  1068. startingY += offset;
  1069. }
  1070. $( 'body' ).mousemove( trackMouse );
  1071. $( 'body' ).one( 'mouseup', endDrag );
  1072. },
  1073. /** adjust the parition up/down +/-adj pixels */
  1074. adjPartition : function( adj ){
  1075. var $unpaired = this.$( '.unpaired-columns' ),
  1076. $paired = this.$( '.paired-columns' ),
  1077. unpairedHi = parseInt( $unpaired.css( 'height' ), 10 ),
  1078. pairedHi = parseInt( $paired.css( 'height' ), 10 );
  1079. //this.debug( adj, 'hi\'s:', unpairedHi, pairedHi, unpairedHi + adj, pairedHi - adj );
  1080. unpairedHi = Math.max( 10, unpairedHi + adj );
  1081. pairedHi = pairedHi - adj;
  1082. var movingUpwards = adj < 0;
  1083. // when the divider gets close to the top - lock into hiding the unpaired section
  1084. if( movingUpwards ){
  1085. if( this.unpairedPanelHidden ){
  1086. return false;
  1087. } else if( unpairedHi <= 10 ){
  1088. this.hideUnpaired();
  1089. return false;
  1090. }
  1091. } else {
  1092. if( this.unpairedPanelHidden ){
  1093. $unpaired.show();
  1094. this.unpairedPanelHidden = false;
  1095. }
  1096. }
  1097. // when the divider gets close to the bottom - lock into hiding the paired section
  1098. if( !movingUpwards ){
  1099. if( this.pairedPanelHidden ){
  1100. return false;
  1101. } else if( pairedHi <= 15 ){
  1102. this.hidePaired();
  1103. return false;
  1104. }
  1105. } else {
  1106. if( this.pairedPanelHidden ){
  1107. $paired.show();
  1108. this.pairedPanelHidden = false;
  1109. }
  1110. }
  1111. $unpaired.css({
  1112. height : unpairedHi + 'px',
  1113. flex : '0 0 auto'
  1114. });
  1115. return true;
  1116. },
  1117. // ........................................................................ paired
  1118. /** select a pair when clicked */
  1119. selectPair : function( ev ){
  1120. ev.stopPropagation();
  1121. $( ev.currentTarget ).toggleClass( 'selected' );
  1122. },
  1123. /** deselect all pairs */
  1124. clearSelectedPaired : function( ev ){
  1125. this.$( '.paired-columns .dataset.selected' ).removeClass( 'selected' );
  1126. },
  1127. /** rename a pair when the pair name is clicked */
  1128. _clickPairName : function( ev ){
  1129. ev.stopPropagation();
  1130. var $name = $( ev.currentTarget ),
  1131. $pair = $name.parent().parent(),
  1132. index = $pair.index( '.dataset.paired' ),
  1133. pair = this.paired[ index ],
  1134. response = prompt( 'Enter a new name for the pair:', pair.name );
  1135. if( response ){
  1136. pair.name = response;
  1137. // set a flag (which won't be passed in json creation) for manual naming so we don't overwrite these
  1138. // when adding/removing extensions
  1139. //hackish
  1140. pair.customizedName = true;
  1141. $name.text( pair.name );
  1142. }
  1143. },
  1144. /** unpair this pair */
  1145. _clickUnpair : function( ev ){
  1146. //if( !ev.currentTarget ){ return true; }
  1147. var pairIndex = Math.floor( $( ev.currentTarget ).index( '.unpair-btn' ) );
  1148. //this.debug( 'pair:', pairIndex );
  1149. this._unpair( this.paired[ pairIndex ] );
  1150. },
  1151. // ........................................................................ paired - drag and drop re-ordering
  1152. //_dragenterPairedColumns : function( ev ){
  1153. // this.debug( '_dragenterPairedColumns:', ev );
  1154. //},
  1155. //_dragleavePairedColumns : function( ev ){
  1156. // //this.debug( '_dragleavePairedColumns:', ev );
  1157. //},
  1158. /** track the mouse drag over the paired list adding a placeholder to show where the drop would occur */
  1159. _dragoverPairedColumns : function( ev ){
  1160. //this.debug( '_dragoverPairedColumns:', ev );
  1161. ev.preventDefault();
  1162. var $list = this.$( '.paired-columns .column-datasets' );
  1163. this._checkForAutoscroll( $list, ev.originalEvent.clientY );
  1164. //this.debug( ev.originalEvent.clientX, ev.originalEvent.clientY );
  1165. var $nearest = this._getNearestPairedDatasetLi( ev.originalEvent.clientY );
  1166. $( '.paired-drop-placeholder' ).remove();
  1167. var $placeholder = $( '<div class="paired-drop-placeholder"></div>' );
  1168. if( !$nearest.size() ){
  1169. $list.append( $placeholder );
  1170. } else {
  1171. $nearest.before( $placeholder );
  1172. }
  1173. },
  1174. /** If the mouse is near enough to the list's top or bottom, scroll the list */
  1175. _checkForAutoscroll : function( $element, y ){
  1176. var AUTOSCROLL_SPEED = 2;
  1177. var offset = $element.offset(),
  1178. scrollTop = $element.scrollTop(),
  1179. upperDist = y - offset.top,
  1180. lowerDist = ( offset.top + $element.outerHeight() ) - y;
  1181. //this.debug( '_checkForAutoscroll:', scrollTop, upperDist, lowerDist );
  1182. if( upperDist >= 0 && upperDist < this.autoscrollDist ){
  1183. $element.scrollTop( scrollTop - AUTOSCROLL_SPEED );
  1184. } else if( lowerDist >= 0 && lowerDist < this.autoscrollDist ){
  1185. $element.scrollTop( scrollTop + AUTOSCROLL_SPEED );
  1186. }
  1187. },
  1188. /** get the nearest *previous* paired dataset PairView based on the mouse's Y coordinate.
  1189. * If the y is at the end of the list, return an empty jQuery object.
  1190. */
  1191. _getNearestPairedDatasetLi : function( y ){
  1192. var WIGGLE = 4,
  1193. lis = this.$( '.paired-columns .column-datasets li' ).toArray();
  1194. for( var i=0; i<lis.length; i++ ){
  1195. var $li = $( lis[i] ),
  1196. top = $li.offset().top,
  1197. halfHeight = Math.floor( $li.outerHeight() / 2 ) + WIGGLE;
  1198. if( top + halfHeight > y && top - halfHeight < y ){
  1199. //this.debug( y, top + halfHeight, top - halfHeight )
  1200. return $li;
  1201. }
  1202. }
  1203. return $();
  1204. },
  1205. /** drop (dragged/selected PairViews) onto the list, re-ordering both the DOM and the internal array of pairs */
  1206. _dropPairedColumns : function( ev ){
  1207. // both required for firefox
  1208. ev.preventDefault();
  1209. ev.dataTransfer.dropEffect = 'move';
  1210. var $nearest = this._getNearestPairedDatasetLi( ev.originalEvent.clientY );
  1211. if( $nearest.size() ){
  1212. this.$dragging.insertBefore( $nearest );
  1213. } else {
  1214. // no nearest before - insert after last element (unpair button)
  1215. this.$dragging.insertAfter( this.$( '.paired-columns .unpair-btn' ).last() );
  1216. }
  1217. // resync the creator's list of paired based on the new DOM order
  1218. this._syncPairsToDom();
  1219. return false;
  1220. },
  1221. /** resync the creator's list of paired based on the DOM order of pairs */
  1222. _syncPairsToDom : function(){
  1223. var newPaired = [];
  1224. //TODO: doesn't seem wise to use the dom to store these - can't we sync another way?
  1225. this.$( '.paired-columns .dataset.paired' ).each( function(){
  1226. newPaired.push( $( this ).data( 'pair' ) );
  1227. });
  1228. //this.debug( newPaired );
  1229. this.paired = newPaired;
  1230. this._renderPaired();
  1231. },
  1232. /** drag communication with pair sub-views: dragstart */
  1233. _pairDragstart : function( ev, pair ){
  1234. //this.debug( '_pairDragstart', ev, pair )
  1235. // auto select the pair causing the event and move all selected
  1236. pair.$el.addClass( 'selected' );
  1237. var $selected = this.$( '.paired-columns .dataset.selected' );
  1238. this.$dragging = $selected;
  1239. },
  1240. /** drag communication with pair sub-views: dragend - remove the placeholder */
  1241. _pairDragend : function( ev, pair ){
  1242. //this.debug( '_pairDragend', ev, pair )
  1243. $( '.paired-drop-placeholder' ).remove();
  1244. this.$dragging = null;
  1245. },
  1246. // ........................................................................ footer
  1247. toggleExtensions : function( force ){
  1248. var creator = this;
  1249. creator.removeExtensions = ( force !== undefined )?( force ):( !creator.removeExtensions );
  1250. _.each( creator.paired, function( pair ){
  1251. // don't overwrite custom names
  1252. if( pair.customizedName ){ return; }
  1253. pair.name = creator._guessNameForPair( pair.forward, pair.reverse );
  1254. });
  1255. creator._renderPaired();
  1256. creator._renderFooter();
  1257. },
  1258. /** handle a collection name change */
  1259. _changeName : function( ev ){
  1260. this._validationWarning( 'name', !!this._getName() );
  1261. },
  1262. /** check for enter key press when in the collection name and submit */
  1263. _nameCheckForEnter : function( ev ){
  1264. if( ev.keyCode === 13 ){
  1265. this._clickCreate();
  1266. }
  1267. },
  1268. /** get the current collection name */
  1269. _getName : function(){
  1270. return _.escape( this.$( '.collection-name' ).val() );
  1271. },
  1272. /** attempt to create the current collection */
  1273. _clickCreate : function( ev ){
  1274. var name = this._getName();
  1275. if( !name ){
  1276. this._validationWarning( 'name' );
  1277. } else {
  1278. this.createList();
  1279. }
  1280. },
  1281. // ------------------------------------------------------------------------ misc
  1282. /** debug a dataset list */
  1283. _printList : function( list ){
  1284. var creator = this;
  1285. _.each( list, function( e ){
  1286. if( list === creator.paired ){
  1287. creator._printPair( e );
  1288. } else {
  1289. //creator.debug( e );
  1290. }
  1291. });
  1292. },
  1293. /** print a pair Object */
  1294. _printPair : function( pair ){
  1295. this.debug( pair.forward.name, pair.reverse.name, ': ->', pair.name );
  1296. },
  1297. /** string rep */
  1298. toString : function(){ return 'PairedCollectionCreator'; }
  1299. });
  1300. //TODO: move to require text plugin and load these as text
  1301. //TODO: underscore currently unnecc. bc no vars are used
  1302. //TODO: better way of localizing text-nodes in long strings
  1303. /** underscore template fns attached to class */
  1304. PairedCollectionCreator.templates = PairedCollectionCreator.templates || {
  1305. /** the skeleton */
  1306. main : _.template([
  1307. '<div class="header flex-row no-flex"></div>',
  1308. '<div class="middle flex-row flex-row-container"></div>',
  1309. '<div class="footer flex-row no-flex">'
  1310. ].join('')),
  1311. /** the header (not including help text) */
  1312. header : _.template([
  1313. '<div class="main-help well clear">',
  1314. '<a class="more-help" href="javascript:void(0);">', _l( 'More help' ), '</a>',
  1315. '<div class="help-content">',
  1316. '<a class="less-help" href="javascript:void(0);">', _l( 'Less' ), '</a>',
  1317. '</div>',
  1318. '</div>',
  1319. '<div class="alert alert-dismissable">',
  1320. '<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>',
  1321. '<span class="alert-message"></span>',
  1322. '</div>',
  1323. '<div class="column-headers vertically-spaced flex-column-container">',
  1324. '<div class="forward-column flex-column column">',
  1325. '<div class="column-header">',
  1326. '<div class="column-title">',
  1327. '<span class="title">', _l( 'Unpaired forward' ), '</span>',
  1328. '<span class="title-info unpaired-info"></span>',
  1329. '</div>',
  1330. '<div class="unpaired-filter forward-unpaired-filter pull-left">',
  1331. '<input class="search-query" placeholder="', _l( 'Filter this list' ), '" />',
  1332. '</div>',
  1333. '</div>',
  1334. '</div>',
  1335. '<div class="paired-column flex-column no-flex column">',
  1336. '<div class="column-header">',
  1337. '<a class="choose-filters-link" href="javascript:void(0)">',
  1338. _l( 'Choose filters' ),
  1339. '</a>',
  1340. '<a class="clear-filters-link" href="javascript:void(0);">',
  1341. _l( 'Clear filters' ),
  1342. '</a><br />',
  1343. '<a class="autopair-link" href="javascript:void(0);">',
  1344. _l( 'Auto-pair' ),
  1345. '</a>',
  1346. '</div>',
  1347. '</div>',
  1348. '<div class="reverse-column flex-column column">',
  1349. '<div class="column-header">',
  1350. '<div class="column-title">',
  1351. '<span class="title">', _l( 'Unpaired reverse' ), '</span>',
  1352. '<span class="title-info unpaired-info"></span>',
  1353. '</div>',
  1354. '<div class="unpaired-filter reverse-unpaired-filter pull-left">',
  1355. '<input class="search-query" placeholder="', _l( 'Filter this list' ), '" />',
  1356. '</div>',
  1357. '</div>',
  1358. '</div>',
  1359. '</div>'
  1360. ].join('')),
  1361. /** the middle: unpaired, divider, and paired */
  1362. middle : _.template([
  1363. // contains two flex rows (rows that fill available space) and a divider btwn
  1364. '<div class="unpaired-columns flex-column-container scroll-container flex-row">',
  1365. '<div class="forward-column flex-column column">',
  1366. '<ol class="column-datasets"></ol>',
  1367. '</div>',
  1368. '<div class="paired-column flex-column no-flex column">',
  1369. '<ol class="column-datasets"></ol>',
  1370. '</div>',
  1371. '<div class="reverse-column flex-column column">',
  1372. '<ol class="column-datasets"></ol>',
  1373. '</div>',
  1374. '</div>',
  1375. '<div class="flexible-partition">',
  1376. '<div class="flexible-partition-drag" title="', _l( 'Drag to change' ), '"></div>',
  1377. '<div class="column-header">',
  1378. '<div class="column-title paired-column-title">',
  1379. '<span class="title"></span>',
  1380. '</div>',
  1381. '<a class="unpair-all-link" href="javascript:void(0);">',
  1382. _l( 'Unpair all' ),
  1383. '</a>',
  1384. '</div>',
  1385. '</div>',
  1386. '<div class="paired-columns flex-column-container scroll-container flex-row">',
  1387. '<ol class="column-datasets"></ol>',
  1388. '</div>'
  1389. ].join('')),
  1390. /** creation and cancel controls */
  1391. footer : _.template([
  1392. '<div class="attributes clear">',
  1393. '<div class="clear">',
  1394. '<label class="remove-extensions-prompt pull-right">',
  1395. _l( 'Remove file extensions from pair names' ), '?',
  1396. '<input class="remove-extensions pull-right" type="checkbox" />',
  1397. '</label>',
  1398. '</div>',
  1399. '<div class="clear">',
  1400. '<input class="collection-name form-control pull-right" ',
  1401. 'placeholder="', _l( 'Enter a name for your new list' ), '" />',
  1402. '<div class="collection-name-prompt pull-right">', _l( 'Name' ), ':</div>',
  1403. '</div>',
  1404. '</div>',
  1405. '<div class="actions clear vertically-spaced">',
  1406. '<div class="other-options pull-left">',
  1407. '<button class="cancel-create btn" tabindex="-1">', _l( 'Cancel' ), '</button>',
  1408. '<div class="create-other btn-group dropup">',
  1409. '<button class="btn btn-default dropdown-toggle" data-toggle="dropdown">',
  1410. _l( 'Create a different kind of collection' ),
  1411. ' <span class="caret"></span>',
  1412. '</button>',
  1413. '<ul class="dropdown-menu" role="menu">',
  1414. '<li><a href="#">', _l( 'Create a <i>single</i> pair' ), '</a></li>',
  1415. '<li><a href="#">', _l( 'Create a list of <i>unpaired</i> datasets' ), '</a></li>',
  1416. '</ul>',
  1417. '</div>',
  1418. '</div>',
  1419. '<div class="main-options pull-right">',
  1420. '<button class="create-collection btn btn-primary">', _l( 'Create list' ), '</button>',
  1421. '</div>',
  1422. '</div>'
  1423. ].join('')),
  1424. /** help content */
  1425. helpContent : _.template([
  1426. '<p>', _l([
  1427. 'Collections of paired datasets are ordered lists of dataset pairs (often forward and reverse reads). ',
  1428. 'These collections can be passed to tools and workflows in order to have analyses done on each member of ',
  1429. 'the entire group. This interface allows you to create a collection, choose which datasets are paired, ',
  1430. 'and re-order the final collection.'
  1431. ].join( '' )), '</p>',
  1432. '<p>', _l([
  1433. 'Unpaired datasets are shown in the <i data-target=".unpaired-columns">unpaired section</i> ',
  1434. '(hover over the underlined words to highlight below). ',
  1435. 'Paired datasets are shown in the <i data-target=".paired-columns">paired section</i>.',
  1436. '<ul>To pair datasets, you can:',
  1437. '<li>Click a dataset in the ',
  1438. '<i data-target=".unpaired-columns .forward-column .column-datasets,',
  1439. '.unpaired-columns .forward-column">forward column</i> ',
  1440. 'to select it then click a dataset in the ',
  1441. '<i data-target=".unpaired-columns .reverse-column .column-datasets,',
  1442. '.unpaired-columns .reverse-column">reverse column</i>.',
  1443. '</li>',
  1444. '<li>Click one of the "Pair these datasets" buttons in the ',
  1445. '<i data-target=".unpaired-columns .paired-column .column-datasets,',
  1446. '.unpaired-columns .paired-column">middle column</i> ',
  1447. 'to pair the datasets in a particular row.',
  1448. '</li>',
  1449. '<li>Click <i data-target=".autopair-link">"Auto-pair"</i> ',
  1450. 'to have your datasets automatically paired based on name.',
  1451. '</li>',
  1452. '</ul>'
  1453. ].join( '' )), '</p>',
  1454. '<p>', _l([
  1455. '<ul>You can filter what is shown in the unpaired sections by:',
  1456. '<li>Entering partial dataset names in either the ',
  1457. '<i data-target=".forward-unpaired-filter input">forward filter</i> or ',
  1458. '<i data-target=".reverse-unpaired-filter input">reverse filter</i>.',
  1459. '</li>',
  1460. '<li>Choosing from a list of preset filters by clicking the ',
  1461. '<i data-target=".choose-filters-link">"Choose filters" link</i>.',
  1462. '</li>',
  1463. '<li>Entering regular expressions to match dataset names. See: ',
  1464. '<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions"',
  1465. ' target="_blank">MDN\'s JavaScript Regular Expression Tutorial</a>. ',
  1466. 'Note: forward slashes (\\) are not needed.',
  1467. '</li>',
  1468. '<li>Clearing the filters by clicking the ',
  1469. '<i data-target=".clear-filters-link">"Clear filters" link</i>.',
  1470. '</li>',
  1471. '</ul>'
  1472. ].join( '' )), '</p>',
  1473. '<p>', _l([
  1474. 'To unpair individual dataset pairs, click the ',
  1475. '<i data-target=".unpair-btn">unpair buttons ( <span class="fa fa-unlink"></span> )</i>. ',
  1476. 'Click the <i data-target=".unpair-all-link">"Unpair all" link</i> to unpair all pairs.'
  1477. ].join( '' )), '</p>',
  1478. '<p>', _l([
  1479. 'You can include or remove the file extensions (e.g. ".fastq") from your pair names by toggling the ',
  1480. '<i data-target=".remove-extensions-prompt">"Remove file extensions from pair names?"</i> control.'
  1481. ].join( '' )), '</p>',
  1482. '<p>', _l([
  1483. 'Once your collection is complete, enter a <i data-target=".collection-name">name</i> and ',
  1484. 'click <i data-target=".create-collection">"Create list"</i>. ',
  1485. '(Note: you do not have to pair all unpaired datasets to finish.)'
  1486. ].join( '' )), '</p>'
  1487. ].join(''))
  1488. };
  1489. //=============================================================================
  1490. (function(){
  1491. /** plugin that will highlight an element when this element is hovered over */
  1492. jQuery.fn.extend({
  1493. hoverhighlight : function $hoverhighlight( scope, color ){
  1494. scope = scope || 'body';
  1495. if( !this.size() ){ return this; }
  1496. $( this ).each( function(){
  1497. var $this = $( this ),
  1498. targetSelector = $this.data( 'target' );
  1499. if( targetSelector ){
  1500. $this.mouseover( function( ev ){
  1501. $( targetSelector, scope ).css({
  1502. background: color
  1503. });
  1504. })
  1505. .mouseout( function( ev ){
  1506. $( targetSelector ).css({
  1507. background: ''
  1508. });
  1509. });
  1510. }
  1511. });
  1512. return this;
  1513. }
  1514. });
  1515. }());
  1516. //=============================================================================
  1517. /** a modal version of the paired collection creator */
  1518. var pairedCollectionCreatorModal = function _pairedCollectionCreatorModal( datasets, options ){
  1519. options = _.defaults( options || {}, {
  1520. datasets : datasets,
  1521. oncancel : function(){ Galaxy.modal.hide(); },
  1522. oncreate : function(){
  1523. Galaxy.modal.hide();
  1524. Galaxy.currHistoryPanel.refreshContents();
  1525. }
  1526. });
  1527. if( !window.Galaxy || !Galaxy.modal ){
  1528. throw new Error( 'Galaxy or Galaxy.modal not found' );
  1529. }
  1530. var creator = new PairedCollectionCreator( options );
  1531. Galaxy.modal.show({
  1532. title : 'Create a collection of paired datasets',
  1533. body : creator.$el,
  1534. width : '80%',
  1535. height : '800px',
  1536. closing_events: true
  1537. });
  1538. creator.render();
  1539. //TODO: remove modal header
  1540. window.PCC = creator;
  1541. return creator;
  1542. };
  1543. //=============================================================================
  1544. return {
  1545. PairedCollectionCreator : PairedCollectionCreator,
  1546. pairedCollectionCreatorModal : pairedCollectionCreatorModal
  1547. };
  1548. });