PageRenderTime 36ms CodeModel.GetById 24ms RepoModel.GetById 0ms app.codeStats 0ms

/client/galaxy/scripts/mvc/collection/paired-collection-creator.js

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