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

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

https://bitbucket.org/kellrott/galaxy-central
JavaScript | 1537 lines | 1098 code | 144 blank | 295 comment | 123 complexity | fa92e6877b508a167cc2211ef9f0b02b MD5 | raw file
Possible License(s): CC-BY-3.0

Large files files are truncated, but you can click here to view the full file

  1. define([
  2. "utils/levenshtein",
  3. "mvc/base-mvc",
  4. "utils/localization"
  5. ], function( levelshteinDistance, baseMVC, _l ){
  6. //=============================================================================
  7. /** An interface for building collections of paired datasets.
  8. */
  9. var PairedCollectionCreator = Backbone.View.extend( baseMVC.LoggableMixin ).extend({
  10. className: 'collection-creator flex-row-container',
  11. /** set up initial options, instance vars, behaviors, and autopair (if set to do so) */
  12. initialize : function( attributes ){
  13. //console.debug( '-- PairedCollectionCreator:', attributes );
  14. attributes = _.defaults( attributes, {
  15. datasets : [],
  16. filters : this.DEFAULT_FILTERS,
  17. automaticallyPair : true,
  18. matchPercentage : 1.0,
  19. //matchPercentage : 0.9,
  20. //matchPercentage : 0.8,
  21. //strategy : 'levenshtein'
  22. strategy : 'lcs'
  23. });
  24. //console.debug( 'attributes now:', attributes );
  25. /** unordered, original list */
  26. this.initialList = attributes.datasets;
  27. /** is this from a history? if so, what's its id? */
  28. this.historyId = attributes.historyId;
  29. /** which filters should be used initially? (String[2] or name in commonFilters) */
  30. this.filters = this.commonFilters[ attributes.filters ] || this.commonFilters[ this.DEFAULT_FILTERS ];
  31. if( _.isArray( attributes.filters ) ){
  32. this.filters = attributes.filters;
  33. }
  34. /** try to auto pair the unpaired datasets on load? */
  35. this.automaticallyPair = attributes.automaticallyPair;
  36. /** distance/mismatch level allowed for autopairing */
  37. this.matchPercentage = attributes.matchPercentage;
  38. /** what method to use for auto pairing (will be passed aggression level) */
  39. this.strategy = this.strategies[ attributes.strategy ] || this.strategies[ this.DEFAULT_STRATEGY ];
  40. if( _.isFunction( attributes.strategy ) ){
  41. this.strategy = attributes.strategy;
  42. }
  43. /** remove file extensions (\.*) from created pair names? */
  44. this.removeExtensions = true;
  45. //this.removeExtensions = false;
  46. /** fn to call when the cancel button is clicked (scoped to this) - if falsy, no btn is displayed */
  47. this.oncancel = attributes.oncancel;
  48. /** fn to call when the collection is created (scoped to this) */
  49. this.oncreate = attributes.oncreate;
  50. /** is the unpaired panel shown? */
  51. this.unpairedPanelHidden = false;
  52. /** is the paired panel shown? */
  53. this.pairedPanelHidden = false;
  54. this._dataSetUp();
  55. this._setUpBehaviors();
  56. },
  57. /** map of common filter pairs by name */
  58. commonFilters : {
  59. none : [ '', '' ],
  60. illumina : [ '_1', '_2' ]
  61. },
  62. /** which commonFilter to use by default */
  63. DEFAULT_FILTERS : 'illumina',
  64. //DEFAULT_FILTERS : 'none',
  65. /** map of name->fn for autopairing */
  66. strategies : {
  67. 'lcs' : 'autoPairLCSs',
  68. 'levenshtein' : 'autoPairLevenshtein'
  69. },
  70. /** default autopair strategy name */
  71. DEFAULT_STRATEGY : 'lcs',
  72. // ------------------------------------------------------------------------ process raw list
  73. /** set up main data: cache initialList, sort, and autopair */
  74. _dataSetUp : function(){
  75. //console.debug( '-- _dataSetUp' );
  76. this.paired = [];
  77. this.unpaired = [];
  78. //this.fwdSelectedIds = [];
  79. //this.revSelectedIds = [];
  80. this.selectedIds = [];
  81. // sort initial list, add ids if needed, and save new working copy to unpaired
  82. this._sortInitialList();
  83. this._ensureIds();
  84. this.unpaired = this.initialList.slice( 0 );
  85. if( this.automaticallyPair ){
  86. this.autoPair();
  87. }
  88. },
  89. /** sort initial list */
  90. _sortInitialList : function(){
  91. //console.debug( '-- _sortInitialList' );
  92. this._sortDatasetList( this.initialList );
  93. //this._printList( this.unpaired );
  94. },
  95. /** sort a list of datasets */
  96. _sortDatasetList : function( list ){
  97. // currently only natural sort by name
  98. list.sort( function( a, b ){ return naturalSort( a.name, b.name ); });
  99. return list;
  100. },
  101. /** add ids to dataset objs in initial list if none */
  102. _ensureIds : function(){
  103. this.initialList.forEach( function( dataset ){
  104. if( !dataset.hasOwnProperty( 'id' ) ){
  105. dataset.id = _.uniqueId();
  106. }
  107. });
  108. //this._printList( this.unpaired );
  109. return this.initialList;
  110. },
  111. /** split initial list into two lists, those that pass forward filters & those passing reverse */
  112. _splitByFilters : function( filters ){
  113. var fwd = [],
  114. rev = [];
  115. this.unpaired.forEach( function( unpaired ){
  116. if( this._filterFwdFn( unpaired ) ){
  117. fwd.push( unpaired );
  118. }
  119. if( this._filterRevFn( unpaired ) ){
  120. rev.push( unpaired );
  121. }
  122. }.bind( this ) );
  123. return [ fwd, rev ];
  124. },
  125. /** filter fn to apply to forward datasets */
  126. _filterFwdFn : function( dataset ){
  127. //TODO: this treats *all* strings as regex which may confuse people
  128. var regexp = new RegExp( this.filters[0] );
  129. return regexp.test( dataset.name );
  130. //return dataset.name.indexOf( this.filters[0] ) >= 0;
  131. },
  132. /** filter fn to apply to reverse datasets */
  133. _filterRevFn : function( dataset ){
  134. var regexp = new RegExp( this.filters[1] );
  135. return regexp.test( dataset.name );
  136. },
  137. /** add a dataset to the unpaired list in it's proper order */
  138. _addToUnpaired : function( dataset ){
  139. // currently, unpaired is natural sorted by name, use binary search to find insertion point
  140. var binSearchSortedIndex = function( low, hi ){
  141. if( low === hi ){ return low; }
  142. var mid = Math.floor( ( hi - low ) / 2 ) + low,
  143. compared = naturalSort( dataset.name, this.unpaired[ mid ].name );
  144. if( compared < 0 ){
  145. return binSearchSortedIndex( low, mid );
  146. } else if( compared > 0 ){
  147. return binSearchSortedIndex( mid + 1, hi );
  148. }
  149. // walk the equal to find the last
  150. while( this.unpaired[ mid ] && this.unpaired[ mid ].name === dataset.name ){ mid++; }
  151. return mid;
  152. }.bind( this );
  153. this.unpaired.splice( binSearchSortedIndex( 0, this.unpaired.length ), 0, dataset );
  154. },
  155. // ------------------------------------------------------------------------ auto pairing
  156. //TODO: lots of boiler plate btwn the three auto pair fns
  157. /** two passes to automatically create pairs:
  158. * use both simpleAutoPair, then the fn mentioned in strategy
  159. */
  160. autoPair : function( strategy ){
  161. strategy = strategy || this.strategy;
  162. //console.debug( '-- autoPair', strategy );
  163. this.simpleAutoPair();
  164. return this[ strategy ].call( this );
  165. },
  166. /** attempts to pair forward with reverse when names exactly match (after removing filters) */
  167. simpleAutoPair : function(){
  168. //console.debug( '-- simpleAutoPair' );
  169. // simplified auto pair that moves down unpaired lists *in order*,
  170. // removes filters' strings from fwd and rev,
  171. // and, if names w/o filters *exactly* match, creates a pair
  172. // possibly good as a first pass
  173. var i = 0, j,
  174. split = this._splitByFilters(),
  175. fwdList = split[0],
  176. revList = split[1],
  177. fwdName, revName,
  178. matchFound = false;
  179. while( i<fwdList.length ){
  180. var fwd = fwdList[ i ];
  181. //TODO: go through the filterFwdFn
  182. fwdName = fwd.name.replace( this.filters[0], '' );
  183. //console.debug( i, 'fwd:', fwdName );
  184. matchFound = false;
  185. for( j=0; j<revList.length; j++ ){
  186. var rev = revList[ j ];
  187. revName = rev.name.replace( this.filters[1], '' );
  188. //console.debug( '\t ', j, 'rev:', revName );
  189. if( fwd !== rev && fwdName === revName ){
  190. matchFound = true;
  191. // if it is a match, keep i at current, pop fwd, pop rev and break
  192. //console.debug( '---->', fwdName, revName );
  193. this._pair(
  194. fwdList.splice( i, 1 )[0],
  195. revList.splice( j, 1 )[0],
  196. { silent: true }
  197. );
  198. break;
  199. }
  200. }
  201. if( !matchFound ){ i += 1; }
  202. }
  203. //console.debug( 'remaining Forward:' );
  204. //this._printList( this.unpairedForward );
  205. //console.debug( 'remaining Reverse:' );
  206. //this._printList( this.unpairedReverse );
  207. //console.debug( '' );
  208. },
  209. /** attempt to autopair using edit distance between forward and reverse (after removing filters) */
  210. autoPairLevenshtein : function(){
  211. //precondition: filters are set, both lists are not empty, and all filenames.length > filters[?].length
  212. //console.debug( '-- autoPairLevenshtein' );
  213. var i = 0, j,
  214. split = this._splitByFilters(),
  215. fwdList = split[0],
  216. revList = split[1],
  217. fwdName, revName,
  218. distance, bestIndex, bestDist;
  219. while( i<fwdList.length ){
  220. var fwd = fwdList[ i ];
  221. //TODO: go through the filterFwdFn
  222. fwdName = fwd.name.replace( this.filters[0], '' );
  223. //console.debug( i, 'fwd:', fwdName );
  224. bestDist = Number.MAX_VALUE;
  225. for( j=0; j<revList.length; j++ ){
  226. var rev = revList[ j ];
  227. revName = rev.name.replace( this.filters[1], '' );
  228. //console.debug( '\t ', j, 'rev:', revName );
  229. if( fwd !== rev ){
  230. if( fwdName === revName ){
  231. //console.debug( '\t\t exactmatch:', fwdName, revName );
  232. bestIndex = j;
  233. bestDist = 0;
  234. break;
  235. }
  236. distance = levenshteinDistance( fwdName, revName );
  237. //console.debug( '\t\t distance:', distance, 'bestDist:', bestDist );
  238. if( distance < bestDist ){
  239. bestIndex = j;
  240. bestDist = distance;
  241. }
  242. }
  243. }
  244. //console.debug( '---->', fwd.name, bestIndex, bestDist );
  245. //console.debug( '---->', fwd.name, revList[ bestIndex ].name, bestDist );
  246. var percentage = 1.0 - ( bestDist / ( Math.max( fwdName.length, revName.length ) ) );
  247. //console.debug( '----> %', percentage * 100 );
  248. if( percentage >= this.matchPercentage ){
  249. this._pair(
  250. fwdList.splice( i, 1 )[0],
  251. revList.splice( bestIndex, 1 )[0],
  252. { silent: true }
  253. );
  254. if( fwdList.length <= 0 || revList.length <= 0 ){
  255. return;
  256. }
  257. } else {
  258. i += 1;
  259. }
  260. }
  261. //console.debug( 'remaining Forward:' );
  262. //this._printList( this.unpairedForward );
  263. //console.debug( 'remaining Reverse:' );
  264. //this._printList( this.unpairedReverse );
  265. //console.debug( '' );
  266. },
  267. /** attempt to auto pair using common substrings from both front and back (after removing filters) */
  268. autoPairLCSs : function(){
  269. //precondition: filters are set, both lists are not empty
  270. //console.debug( '-- autoPairLCSs' );
  271. var i = 0, j,
  272. split = this._splitByFilters(),
  273. fwdList = split[0],
  274. revList = split[1],
  275. fwdName, revName,
  276. currMatch, bestIndex, bestMatch;
  277. if( !fwdList.length || !revList.length ){ return; }
  278. //console.debug( fwdList, revList );
  279. while( i<fwdList.length ){
  280. var fwd = fwdList[ i ];
  281. fwdName = fwd.name.replace( this.filters[0], '' );
  282. //console.debug( i, 'fwd:', fwdName );
  283. bestMatch = 0;
  284. for( j=0; j<revList.length; j++ ){
  285. var rev = revList[ j ];
  286. revName = rev.name.replace( this.filters[1], '' );
  287. //console.debug( '\t ', j, 'rev:', revName );
  288. if( fwd !== rev ){
  289. if( fwdName === revName ){
  290. //console.debug( '\t\t exactmatch:', fwdName, revName );
  291. bestIndex = j;
  292. bestMatch = fwdName.length;
  293. break;
  294. }
  295. var match = this._naiveStartingAndEndingLCS( fwdName, revName );
  296. currMatch = match.length;
  297. //console.debug( '\t\t match:', match, 'currMatch:', currMatch, 'bestMatch:', bestMatch );
  298. if( currMatch > bestMatch ){
  299. bestIndex = j;
  300. bestMatch = currMatch;
  301. }
  302. }
  303. }
  304. //console.debug( '---->', i, fwd.name, bestIndex, revList[ bestIndex ].name, bestMatch );
  305. var percentage = bestMatch / ( Math.min( fwdName.length, revName.length ) );
  306. //console.debug( '----> %', percentage * 100 );
  307. if( percentage >= this.matchPercentage ){
  308. this._pair(
  309. fwdList.splice( i, 1 )[0],
  310. revList.splice( bestIndex, 1 )[0],
  311. { silent: true }
  312. );
  313. if( fwdList.length <= 0 || revList.length <= 0 ){
  314. return;
  315. }
  316. } else {
  317. i += 1;
  318. }
  319. }
  320. //console.debug( 'remaining Forward:' );
  321. //this._printList( this.unpairedForward );
  322. //console.debug( 'remaining Reverse:' );
  323. //this._printList( this.unpairedReverse );
  324. //console.debug( '' );
  325. },
  326. /** return the concat'd longest common prefix and suffix from two strings */
  327. _naiveStartingAndEndingLCS : function( s1, s2 ){
  328. var fwdLCS = '',
  329. revLCS = '',
  330. i = 0, j = 0;
  331. while( i < s1.length && i < s2.length ){
  332. if( s1[ i ] !== s2[ i ] ){
  333. break;
  334. }
  335. fwdLCS += s1[ i ];
  336. i += 1;
  337. }
  338. if( i === s1.length ){ return s1; }
  339. if( i === s2.length ){ return s2; }
  340. i = ( s1.length - 1 );
  341. j = ( s2.length - 1 );
  342. while( i >= 0 && j >= 0 ){
  343. if( s1[ i ] !== s2[ j ] ){
  344. break;
  345. }
  346. revLCS = [ s1[ i ], revLCS ].join( '' );
  347. i -= 1;
  348. j -= 1;
  349. }
  350. return fwdLCS + revLCS;
  351. },
  352. // ------------------------------------------------------------------------ pairing / unpairing
  353. /** create a pair from fwd and rev, removing them from unpaired, and placing the new pair in paired */
  354. _pair : function( fwd, rev, options ){
  355. options = options || {};
  356. //TODO: eventing, options
  357. //console.debug( '_pair:', fwd, rev );
  358. var pair = this._createPair( fwd, rev, options.name );
  359. this.paired.push( pair );
  360. this.unpaired = _.without( this.unpaired, fwd, rev );
  361. if( !options.silent ){
  362. this.trigger( 'pair:new', pair );
  363. }
  364. return pair;
  365. },
  366. /** create a pair Object from fwd and rev, adding the name attribute (will guess if not given) */
  367. _createPair : function( fwd, rev, name ){
  368. // ensure existance and don't pair something with itself
  369. if( !( fwd && rev ) || ( fwd === rev ) ){
  370. throw new Error( 'Bad pairing: ' + [ JSON.stringify( fwd ), JSON.stringify( rev ) ] );
  371. }
  372. name = name || this._guessNameForPair( fwd, rev );
  373. return { forward : fwd, name : name, reverse : rev };
  374. },
  375. /** try to find a good pair name for the given fwd and rev datasets */
  376. _guessNameForPair : function( fwd, rev, removeExtensions ){
  377. removeExtensions = ( removeExtensions !== undefined )?( removeExtensions ):( this.removeExtensions );
  378. var lcs = this._naiveStartingAndEndingLCS(
  379. //TODO: won't work with regex
  380. fwd.name.replace( this.filters[0], '' ),
  381. rev.name.replace( this.filters[1], '' )
  382. );
  383. if( removeExtensions ){
  384. var lastDotIndex = lcs.lastIndexOf( '.' );
  385. if( lastDotIndex > 0 ){
  386. lcs = lcs.slice( 0, lastDotIndex );
  387. }
  388. }
  389. //TODO: optionally remove extension
  390. return lcs || ( fwd.name + ' & ' + rev.name );
  391. },
  392. ///** find datasets with fwdId and revID and pair them */
  393. //_pairById : function( fwdId, revId, name ){
  394. // var both = this.unpaired.filter( function( unpaired ){
  395. // return unpaired.id === fwdId || unpaired.id === revId;
  396. // }),
  397. // fwd = both[0], rev = both[1];
  398. // if( both[0].id === revId ){
  399. // fwd = rev; rev = both[0];
  400. // }
  401. // return this._pair( fwd, rev, name );
  402. //},
  403. /** unpair a pair, removing it from paired, and adding the fwd,rev datasets back into unpaired */
  404. _unpair : function( pair, options ){
  405. options = options || {};
  406. if( !pair ){
  407. throw new Error( 'Bad pair: ' + JSON.stringify( pair ) );
  408. }
  409. this.paired = _.without( this.paired, pair );
  410. this._addToUnpaired( pair.forward );
  411. this._addToUnpaired( pair.reverse );
  412. if( !options.silent ){
  413. this.trigger( 'pair:unpair', [ pair ] );
  414. }
  415. return pair;
  416. },
  417. /** unpair all paired datasets */
  418. unpairAll : function(){
  419. var pairs = [];
  420. while( this.paired.length ){
  421. pairs.push( this._unpair( this.paired[ 0 ], { silent: true }) );
  422. }
  423. this.trigger( 'pair:unpair', pairs );
  424. },
  425. // ------------------------------------------------------------------------ API
  426. /** convert a pair into JSON compatible with the collections API */
  427. _pairToJSON : function( pair ){
  428. //TODO: consider making this the pair structure when created instead
  429. return {
  430. collection_type : 'paired',
  431. src : 'new_collection',
  432. name : pair.name,
  433. element_identifiers : [{
  434. name : 'forward',
  435. id : pair.forward.id,
  436. //TODO: isn't necessarily true
  437. src : 'hda'
  438. }, {
  439. name : 'reverse',
  440. id : pair.reverse.id,
  441. //TODO: isn't necessarily true
  442. src : 'hda'
  443. }]
  444. };
  445. },
  446. /** create the collection via the API
  447. * @returns {jQuery.xhr Object} the jquery ajax request
  448. */
  449. createList : function(){
  450. var creator = this,
  451. url;
  452. if( creator.historyId ){
  453. url = '/api/histories/' + this.historyId + '/contents/dataset_collections';
  454. //} else {
  455. //
  456. }
  457. //TODO:?? Can't we use ListPairedCollection.create()
  458. var ajaxData = {
  459. type : 'dataset_collection',
  460. collection_type : 'list:paired',
  461. name : _.escape( creator.$( '.collection-name' ).val() ),
  462. element_identifiers : creator.paired.map( function( pair ){
  463. return creator._pairToJSON( pair );
  464. })
  465. };
  466. //console.debug( JSON.stringify( ajaxData ) );
  467. return jQuery.ajax( url, {
  468. type : 'POST',
  469. contentType : 'application/json',
  470. dataType : 'json',
  471. data : JSON.stringify( ajaxData )
  472. })
  473. .fail( function( xhr, status, message ){
  474. creator._ajaxErrHandler( xhr, status, message );
  475. })
  476. .done( function( response, message, xhr ){
  477. //console.info( 'ok', response, message, xhr );
  478. creator.trigger( 'collection:created', response, message, xhr );
  479. if( typeof creator.oncreate === 'function' ){
  480. creator.oncreate.call( this, response, message, xhr );
  481. }
  482. });
  483. },
  484. /** handle ajax errors with feedback and details to the user (if available) */
  485. _ajaxErrHandler : function( xhr, status, message ){
  486. console.error( xhr, status, message );
  487. var content = _l( 'An error occurred while creating this collection' );
  488. if( xhr ){
  489. if( xhr.readyState === 0 && xhr.status === 0 ){
  490. content += ': ' + _l( 'Galaxy could not be reached and may be updating.' )
  491. + _l( ' Try again in a few minutes.' );
  492. } else if( xhr.responseJSON ){
  493. content += '<br /><pre>' + JSON.stringify( xhr.responseJSON ) + '</pre>';
  494. } else {
  495. content += ': ' + message;
  496. }
  497. }
  498. creator._showAlert( content, 'alert-danger' );
  499. },
  500. // ------------------------------------------------------------------------ rendering
  501. /** render the entire interface */
  502. render : function( speed, callback ){
  503. //console.debug( '-- _render' );
  504. //this.$el.empty().html( PairedCollectionCreator.templates.main() );
  505. this.$el.empty().html( PairedCollectionCreator.templates.main() );
  506. this._renderHeader( speed );
  507. this._renderMiddle( speed );
  508. this._renderFooter( speed );
  509. this._addPluginComponents();
  510. return this;
  511. },
  512. /** render the header section */
  513. _renderHeader : function( speed, callback ){
  514. //console.debug( '-- _renderHeader' );
  515. var $header = this.$( '.header' ).empty().html( PairedCollectionCreator.templates.header() )
  516. .find( '.help-content' ).prepend( $( PairedCollectionCreator.templates.helpContent() ) );
  517. this._renderFilters();
  518. return $header;
  519. },
  520. /** fill the filter inputs with the filter values */
  521. _renderFilters : function(){
  522. return this.$( '.forward-column .column-header input' ).val( this.filters[0] )
  523. .add( this.$( '.reverse-column .column-header input' ).val( this.filters[1] ) );
  524. },
  525. /** render the middle including unpaired and paired sections (which may be hidden) */
  526. _renderMiddle : function( speed, callback ){
  527. var $middle = this.$( '.middle' ).empty().html( PairedCollectionCreator.templates.middle() );
  528. // (re-) hide the un/paired panels based on instance vars
  529. //TODO: use replaceWith
  530. if( this.unpairedPanelHidden ){
  531. this.$( '.unpaired-columns' ).hide();
  532. } else if( this.pairedPanelHidden ){
  533. this.$( '.paired-columns' ).hide();
  534. }
  535. this._renderUnpaired();
  536. this._renderPaired();
  537. return $middle;
  538. },
  539. /** render the unpaired section, showing datasets accrd. to filters, update the unpaired counts */
  540. _renderUnpaired : function( speed, callback ){
  541. //console.debug( '-- _renderUnpaired' );
  542. var creator = this,
  543. $fwd, $rev, $prd = [],
  544. split = this._splitByFilters();
  545. // update unpaired counts
  546. this.$( '.forward-column .title' )
  547. .text([ split[0].length, _l( 'unpaired forward' ) ].join( ' ' ));
  548. this.$( '.forward-column .unpaired-info' )
  549. .text( this._renderUnpairedDisplayStr( this.unpaired.length - split[0].length ) );
  550. this.$( '.reverse-column .title' )
  551. .text([ split[1].length, _l( 'unpaired reverse' ) ].join( ' ' ));
  552. this.$( '.reverse-column .unpaired-info' )
  553. .text( this._renderUnpairedDisplayStr( this.unpaired.length - split[1].length ) );
  554. // show/hide the auto pair button if any unpaired are left
  555. this.$( '.autopair-link' ).toggle( this.unpaired.length !== 0 );
  556. if( this.unpaired.length === 0 ){
  557. this._renderUnpairedEmpty();
  558. //this.$( '.unpaired-columns .paired-column .column-datasets' )
  559. // .append( this._renderUnpairedEmpty() );
  560. //TODO: would be best to return here (the $columns)
  561. return;
  562. }
  563. // create the dataset dom arrays
  564. $rev = split[1].map( function( dataset, i ){
  565. // if there'll be a fwd dataset across the way, add a button to pair the row
  566. if( ( split[0][ i ] !== undefined )
  567. && ( split[0][ i ] !== dataset ) ){
  568. $prd.push( creator._renderPairButton() );
  569. }
  570. return creator._renderUnpairedDataset( dataset );
  571. });
  572. $fwd = split[0].map( function( dataset ){
  573. return creator._renderUnpairedDataset( dataset );
  574. });
  575. if( !$fwd.length && !$rev.length ){
  576. this._renderUnpairedNotShown();
  577. return;
  578. }
  579. // add to appropo cols
  580. //TODO: not the best way to render
  581. this.$( '.unpaired-columns .column-datasets' ).empty();
  582. this.$( '.unpaired-columns .forward-column .column-datasets' ).append( $fwd )
  583. .add( this.$( '.unpaired-columns .paired-column .column-datasets' ).append( $prd ) )
  584. .add( this.$( '.unpaired-columns .reverse-column .column-datasets' ).append( $rev ) );
  585. },
  586. /** return a string to display the count of filtered out datasets */
  587. _renderUnpairedDisplayStr : function( numFiltered ){
  588. return [ '(', numFiltered, ' ', _l( 'filtered out' ), ')' ].join('');
  589. },
  590. /** return an unattached jQuery DOM element to represent an unpaired dataset */
  591. _renderUnpairedDataset : function( dataset ){
  592. //TODO: to underscore template
  593. return $( '<li/>')
  594. .attr( 'id', 'dataset-' + dataset.id )
  595. .addClass( 'dataset unpaired' )
  596. .attr( 'draggable', true )
  597. .addClass( dataset.selected? 'selected': '' )
  598. .append( $( '<span/>' ).addClass( 'dataset-name' ).text( dataset.name ) )
  599. //??
  600. .data( 'dataset', dataset );
  601. },
  602. /** render the button that may go between unpaired datasets, allowing the user to pair a row */
  603. _renderPairButton : function(){
  604. //TODO: *not* a dataset - don't pretend like it is
  605. return $( '<li/>').addClass( 'dataset unpaired' )
  606. .append( $( '<span/>' ).addClass( 'dataset-name' ).text( _l( 'Pair these datasets' ) ) );
  607. },
  608. /** a message to display when no unpaired left */
  609. _renderUnpairedEmpty : function(){
  610. //console.debug( '-- renderUnpairedEmpty' );
  611. var $msg = $( '<div class="empty-message"></div>' )
  612. .text( '(' + _l( 'no remaining unpaired datasets' ) + ')' );
  613. this.$( '.unpaired-columns .paired-column .column-datasets' ).empty().prepend( $msg );
  614. return $msg;
  615. },
  616. /** a message to display when no unpaired can be shown with the current filters */
  617. _renderUnpairedNotShown : function(){
  618. //console.debug( '-- renderUnpairedEmpty' );
  619. var $msg = $( '<div class="empty-message"></div>' )
  620. .text( '(' + _l( 'no datasets were found matching the current filters' ) + ')' );
  621. this.$( '.unpaired-columns .paired-column .column-datasets' ).empty().prepend( $msg );
  622. return $msg;
  623. },
  624. /** render the paired section and update counts of paired datasets */
  625. _renderPaired : function( speed, callback ){
  626. //console.debug( '-- _renderPaired' );
  627. var $fwd = [],
  628. $prd = [],
  629. $rev = [];
  630. this.$( '.paired-column-title .title' ).text([ this.paired.length, _l( 'paired' ) ].join( ' ' ) );
  631. // show/hide the unpair all link
  632. this.$( '.unpair-all-link' ).toggle( this.paired.length !== 0 );
  633. if( this.paired.length === 0 ){
  634. this._renderPairedEmpty();
  635. //TODO: would be best to return here (the $columns)
  636. } else {
  637. // show/hide 'remove extensions link' when any paired and they seem to have extensions
  638. this.$( '.remove-extensions-link' ).show();
  639. }
  640. this.paired.forEach( function( pair, i ){
  641. $fwd.push( $( '<li/>').addClass( 'dataset paired' )
  642. .append( $( '<span/>' ).addClass( 'dataset-name' ).text( pair.forward.name ) ) );
  643. $prd.push( $( '<li/>').addClass( 'dataset paired' )
  644. .append( $( '<span/>' ).addClass( 'dataset-name' ).text( pair.name ) ) );
  645. $rev.push( $( '<li/>').addClass( 'dataset paired' )
  646. .append( $( '<span/>' ).addClass( 'dataset-name' ).text( pair.reverse.name ) ) );
  647. $rev.push( $( '<button/>' ).addClass( 'unpair-btn' )
  648. .append( $( '<span/>' ).addClass( 'fa fa-unlink' ).attr( 'title', _l( 'Unpair' ) ) ) );
  649. });
  650. //TODO: better as one swell foop (instead of 4 repaints)
  651. this.$( '.paired-columns .column-datasets' ).empty();
  652. return this.$( '.paired-columns .forward-column .column-datasets' ).prepend( $fwd )
  653. .add( this.$( '.paired-columns .paired-column .column-datasets' ).prepend( $prd ) )
  654. .add( this.$( '.paired-columns .reverse-column .column-datasets' ).prepend( $rev ) );
  655. },
  656. /** a message to display when none paired */
  657. _renderPairedEmpty : function(){
  658. var $msg = $( '<div class="empty-message"></div>' )
  659. .text( '(' + _l( 'no paired datasets yet' ) + ')' );
  660. this.$( '.paired-columns .paired-column .column-datasets' ).prepend( $msg );
  661. return $msg;
  662. },
  663. /** render the footer, completion controls, and cancel controls */
  664. _renderFooter : function( speed, callback ){
  665. var $footer = this.$( '.footer' ).empty().html( PairedCollectionCreator.templates.footer() );
  666. this.$( '.remove-extensions' ).prop( 'checked', this.removeExtensions );
  667. if( typeof this.oncancel === 'function' ){
  668. this.$( '.cancel-create.btn' ).show();
  669. }
  670. return $footer;
  671. },
  672. /** add any jQuery/bootstrap/custom plugins to elements rendered */
  673. _addPluginComponents : function(){
  674. this._chooseFiltersPopover( '.choose-filters-link' );
  675. this.$( '.help-content i' ).hoverhighlight( '.collection-creator', 'rgba( 192, 255, 255, 1.0 )' );
  676. },
  677. /** build a filter selection popover allowing selection of common filter pairs */
  678. _chooseFiltersPopover : function( selector ){
  679. function filterChoice( val1, val2 ){
  680. return [
  681. '<button class="filter-choice btn" ',
  682. 'data-forward="', val1, '" data-reverse="', val2, '">',
  683. _l( 'Forward' ), ': ', val1, ', ',
  684. _l( 'Reverse' ), ': ', val2,
  685. '</button>'
  686. ].join('');
  687. }
  688. var $popoverContent = $( _.template([
  689. '<div class="choose-filters">',
  690. '<div class="help">',
  691. _l( 'Choose from the following filters to change which unpaired reads are shown in the display' ),
  692. ':</div>',
  693. //TODO: connect with common filters
  694. filterChoice( '_1', '_2' ),
  695. filterChoice( '_R1', '_R2' ),
  696. '</div>'
  697. ].join(''), {}));
  698. return this.$( selector ).popover({
  699. container : '.collection-creator',
  700. placement : 'bottom',
  701. html : true,
  702. //animation : false,
  703. content : $popoverContent
  704. });
  705. },
  706. /** add (or clear if clear is truthy) a validation warning to what */
  707. _validationWarning : function( what, clear ){
  708. var VALIDATION_CLASS = 'validation-warning';
  709. if( what === 'name' ){
  710. what = this.$( '.collection-name' ).add( this.$( '.collection-name-prompt' ) );
  711. this.$( '.collection-name' ).focus().select();
  712. }
  713. if( clear ){
  714. what = what || this.$( '.' + VALIDATION_CLASS );
  715. what.removeClass( VALIDATION_CLASS );
  716. } else {
  717. what.addClass( VALIDATION_CLASS );
  718. }
  719. },
  720. // ------------------------------------------------------------------------ events
  721. /** set up event handlers on self */
  722. _setUpBehaviors : function(){
  723. this.on( 'pair:new', function(){
  724. //TODO: ideally only re-render the columns (or even elements) involved
  725. this._renderUnpaired();
  726. this._renderPaired();
  727. // scroll to bottom where new pairs are added
  728. //TODO: this doesn't seem to work - innerHeight sticks at 133...
  729. // may have to do with improper flex columns
  730. //var $pairedView = this.$( '.paired-columns' );
  731. //$pairedView.scrollTop( $pairedView.innerHeight() );
  732. //console.debug( $pairedView.height() )
  733. this.$( '.paired-columns' ).scrollTop( 8000000 );
  734. });
  735. this.on( 'pair:unpair', function( pairs ){
  736. //TODO: ideally only re-render the columns (or even elements) involved
  737. this._renderUnpaired();
  738. this._renderPaired();
  739. this.splitView();
  740. });
  741. this.on( 'filter-change', function(){
  742. this.filters = [
  743. this.$( '.forward-unpaired-filter input' ).val(),
  744. this.$( '.reverse-unpaired-filter input' ).val()
  745. ];
  746. this._renderFilters();
  747. this._renderUnpaired();
  748. });
  749. this.on( 'autopair', function(){
  750. this._renderUnpaired();
  751. this._renderPaired();
  752. var message, msgClass = null;
  753. if( this.paired.length ){
  754. msgClass = 'alert-success';
  755. message = this.paired.length + ' ' + _l( 'pairs created' );
  756. if( !this.unpaired.length ){
  757. message += ': ' + _l( 'all datasets have been successfully paired' );
  758. this.hideUnpaired();
  759. }
  760. } else {
  761. message = _l( 'Could not automatically create any pairs from the given dataset names' );
  762. }
  763. this._showAlert( message, msgClass );
  764. });
  765. //this.on( 'all', function(){
  766. // console.info( arguments );
  767. //});
  768. return this;
  769. },
  770. events : {
  771. // header
  772. 'click .more-help' : '_clickMoreHelp',
  773. 'click .less-help' : '_clickLessHelp',
  774. 'click .header .alert button' : '_hideAlert',
  775. 'click .forward-column .column-title' : '_clickShowOnlyUnpaired',
  776. 'click .reverse-column .column-title' : '_clickShowOnlyUnpaired',
  777. 'click .unpair-all-link' : '_clickUnpairAll',
  778. //TODO: this seems kinda backasswards
  779. 'change .forward-unpaired-filter input' : function( ev ){ this.trigger( 'filter-change' ); },
  780. 'focus .forward-unpaired-filter input' : function( ev ){ $( ev.currentTarget ).select(); },
  781. 'click .autopair-link' : '_clickAutopair',
  782. 'click .choose-filters .filter-choice' : '_clickFilterChoice',
  783. 'click .clear-filters-link' : '_clearFilters',
  784. 'change .reverse-unpaired-filter input' : function( ev ){ this.trigger( 'filter-change' ); },
  785. 'focus .reverse-unpaired-filter input' : function( ev ){ $( ev.currentTarget ).select(); },
  786. // unpaired
  787. 'click .forward-column .dataset.unpaired' : '_clickUnpairedDataset',
  788. 'click .reverse-column .dataset.unpaired' : '_clickUnpairedDataset',
  789. 'click .paired-column .dataset.unpaired' : '_clickPairRow',
  790. 'click .unpaired-columns' : 'clearSelectedUnpaired',
  791. // divider
  792. 'click .paired-column-title' : '_clickShowOnlyPaired',
  793. 'mousedown .flexible-partition-drag' : '_startPartitionDrag',
  794. // paired
  795. 'click .paired-columns .paired-column .dataset-name' : '_clickPairName',
  796. 'mouseover .dataset.paired' : '_hoverPaired',
  797. 'mouseout .dataset.paired' : '_hoverOutPaired',
  798. 'click .unpair-btn' : '_clickUnpair',
  799. // footer
  800. 'change .remove-extensions' : function( ev ){ this.toggleExtensions(); },
  801. 'change .collection-name' : '_changeName',
  802. 'click .cancel-create' : function( ev ){
  803. if( typeof this.oncancel === 'function' ){
  804. this.oncancel.call( this );
  805. }
  806. },
  807. 'click .create-collection' : '_clickCreate'
  808. },
  809. // ........................................................................ header
  810. /** expand help */
  811. _clickMoreHelp : function( ev ){
  812. this.$( '.main-help' ).addClass( 'expanded' );
  813. this.$( '.more-help' ).hide();
  814. },
  815. /** collapse help */
  816. _clickLessHelp : function( ev ){
  817. this.$( '.main-help' ).removeClass( 'expanded' );
  818. this.$( '.more-help' ).show();
  819. },
  820. /** show an alert on the top of the interface containing message (alertClass is bootstrap's alert-*)*/
  821. _showAlert : function( message, alertClass ){
  822. alertClass = alertClass || 'alert-danger';
  823. this.$( '.main-help' ).hide();
  824. this.$( '.header .alert' ).attr( 'class', 'alert alert-dismissable' ).addClass( alertClass ).show()
  825. .find( '.alert-message' ).html( message );
  826. },
  827. /** hide the alerts at the top */
  828. _hideAlert : function( message ){
  829. this.$( '.main-help' ).show();
  830. this.$( '.header .alert' ).hide();
  831. },
  832. //TODO: consolidate these
  833. /** toggle between showing only unpaired and split view */
  834. _clickShowOnlyUnpaired : function( ev ){
  835. //console.debug( 'click unpaired', ev.currentTarget );
  836. if( this.$( '.paired-columns' ).is( ':visible' ) ){
  837. this.hidePaired();
  838. } else {
  839. this.splitView();
  840. }
  841. },
  842. /** toggle between showing only paired and split view */
  843. _clickShowOnlyPaired : function( ev ){
  844. //console.debug( 'click paired' );
  845. if( this.$( '.unpaired-columns' ).is( ':visible' ) ){
  846. this.hideUnpaired();
  847. } else {
  848. this.splitView();
  849. }
  850. },
  851. /** hide unpaired, show paired */
  852. hideUnpaired : function( speed, callback ){
  853. speed = speed || 0;
  854. this.$( '.unpaired-columns' ).hide( speed, callback );
  855. //this.$( '.unpaired-filter' ).hide( speed );
  856. this.$( '.paired-columns' ).show( speed ).css( 'flex', '1 0 auto' );
  857. this.unpairedPanelHidden = true;
  858. },
  859. /** hide paired, show unpaired */
  860. hidePaired : function( speed, callback ){
  861. speed = speed || 0;
  862. this.$( '.unpaired-columns' ).show( speed ).css( 'flex', '1 0 auto' );
  863. //this.$( '.unpaired-filter' ).show( speed );
  864. this.$( '.paired-columns' ).hide( speed, callback );
  865. this.pairedPanelHidden = true;
  866. },
  867. /** show both paired and unpaired (splitting evenly) */
  868. splitView : function( speed, callback ){
  869. speed = speed || 0;
  870. this.unpairedPanelHidden = this.pairedPanelHidden = false;
  871. //this.$( '.unpaired-filter' ).show( speed );
  872. this._renderMiddle( speed );
  873. return this;
  874. },
  875. /** unpair all paired and do other super neat stuff which I'm not really sure about yet... */
  876. _clickUnpairAll : function( ev ){
  877. this.unpairAll();
  878. },
  879. /** attempt to autopair */
  880. _clickAutopair : function( ev ){
  881. var paired = this.autoPair();
  882. //console.debug( 'autopaired', paired );
  883. //TODO: an indication of how many pairs were found - if 0, assist
  884. this.trigger( 'autopair', paired );
  885. },
  886. /** set the filters based on the data attributes of the button click target */
  887. _clickFilterChoice : function( ev ){
  888. var $selected = $( ev.currentTarget );
  889. this.$( '.forward-unpaired-filter input' ).val( $selected.data( 'forward' ) );
  890. this.$( '.reverse-unpaired-filter input' ).val( $selected.data( 'reverse' ) );
  891. this._hideChooseFilters();
  892. this.trigger( 'filter-change' );
  893. },
  894. /** hide the choose filters popover */
  895. _hideChooseFilters : function(){
  896. //TODO: update bootstrap and remove the following hack
  897. // see also: https://github.com/twbs/bootstrap/issues/10260
  898. this.$( '.choose-filters-link' ).popover( 'hide' );
  899. this.$( '.popover' ).css( 'display', 'none' );
  900. },
  901. /** clear both filters */
  902. _clearFilters : function( ev ){
  903. this.$( '.forward-unpaired-filter input' ).val( '' );
  904. this.$( '.reverse-unpaired-filter input' ).val( '' );
  905. this.trigger( 'filter-change' );
  906. },
  907. // ........................................................................ unpaired
  908. /** select an unpaired dataset */
  909. _clickUnpairedDataset : function( ev ){
  910. ev.stopPropagation();
  911. return this.toggleSelectUnpaired( $( ev.currentTarget ) );
  912. },
  913. /** Toggle the selection of an unpaired dataset representation.
  914. * @param [jQuery] $dataset the unpaired dataset dom rep to select
  915. * @param [Boolean] options.force if defined, force selection based on T/F; otherwise, toggle
  916. */
  917. toggleSelectUnpaired : function( $dataset, options ){
  918. options = options || {};
  919. var dataset = $dataset.data( 'dataset' ),
  920. select = options.force !== undefined? options.force: !$dataset.hasClass( 'selected' );
  921. //console.debug( id, options.force, $dataset, dataset );
  922. if( !$dataset.size() || dataset === undefined ){ return $dataset; }
  923. if( select ){
  924. $dataset.addClass( 'selected' );
  925. if( !options.waitToPair ){
  926. this.pairAllSelected();
  927. }
  928. } else {
  929. $dataset.removeClass( 'selected' );
  930. //delete dataset.selected;
  931. }
  932. return $dataset;
  933. },
  934. /** pair all the currently selected unpaired datasets */
  935. pairAllSelected : function( options ){
  936. options = options || {};
  937. var creator = this,
  938. fwds = [],
  939. revs = [],
  940. pairs = [];
  941. //TODO: could be made more concise
  942. creator.$( '.unpaired-columns .forward-column .dataset.selected' ).each( function(){
  943. fwds.push( $( this ).data( 'dataset' ) );
  944. });
  945. creator.$( '.unpaired-columns .reverse-column .dataset.selected' ).each( function(){
  946. revs.push( $( this ).data( 'dataset' ) );
  947. });
  948. fwds.length = revs.length = Math.min( fwds.length, revs.length );
  949. //console.debug( fwds );
  950. //console.debug( revs );
  951. fwds.forEach( function( fwd, i ){
  952. try {
  953. pairs.push( creator._pair( fwd, revs[i], { silent: true }) );
  954. } catch( err ){
  955. //TODO: preserve selected state of those that couldn't be paired
  956. //TODO: warn that some could not be paired
  957. console.error( err );
  958. }
  959. });
  960. if( pairs.length && !options.silent ){
  961. this.trigger( 'pair:new', pairs );
  962. }
  963. return pairs;
  964. },
  965. /** clear the selection on all unpaired datasets */
  966. clearSelectedUnpaired : function(){
  967. this.$( '.unpaired-columns .dataset.selected' ).removeClass( 'selected' );
  968. },
  969. /** attempt to pair two datasets directly across from one another */
  970. _clickPairRow : function( ev ){
  971. //if( !ev.currentTarget ){ return true; }
  972. var rowIndex = $( ev.currentTarget ).index(),
  973. fwd = $( '.unpaired-columns .forward-column .dataset' ).eq( rowIndex ).data( 'dataset' ),
  974. rev = $( '.unpaired-columns .reverse-column .dataset' ).eq( rowIndex ).data( 'dataset' );
  975. //console.debug( 'row:', rowIndex, fwd, rev );
  976. //TODO: animate
  977. this._pair( fwd, rev );
  978. },
  979. // ........................................................................ divider/partition
  980. /** start dragging the visible divider/partition between unpaired and paired panes */
  981. _startPartitionDrag : function( ev ){
  982. var creator = this,
  983. startingY = ev.pageY;
  984. //console.debug( 'partition drag START:', ev );
  985. $( 'body' ).css( 'cursor', 'ns-resize' );
  986. creator.$( '.flexible-partition-drag' ).css( 'color', 'black' );
  987. function endDrag( ev ){
  988. //console.debug( 'partition drag STOP:', ev );
  989. // doing this by an added class didn't really work well - kept flashing still
  990. creator.$( '.flexible-partition-drag' ).css( 'color', '' );
  991. $( 'body' ).css( 'cursor', '' ).unbind( 'mousemove', trackMouse );
  992. }
  993. function trackMouse( ev ){
  994. var offset = ev.pageY - startingY;
  995. if( !creator.adjPartition( offset ) ){
  996. $( 'body' ).trigger( 'mouseup' );
  997. }
  998. startingY += offset;
  999. }
  1000. $( 'body' ).mousemove( trackMouse );
  1001. $( 'body' ).one( 'mouseup', endDrag );
  1002. },
  1003. /** adjust the parition up/down +/-adj pixels */
  1004. adjPartition : function( adj ){
  1005. var $unpaired = this.$( '.unpaired-columns' ),
  1006. $paired = this.$( '.paired-columns' ),
  1007. unpairedHi = parseInt( $unpaired.css( 'height' ), 10 ),
  1008. pairedHi = parseInt( $paired.css( 'height' ), 10 );
  1009. //console.debug( adj, 'hi\'s:', unpairedHi, pairedHi, unpairedHi + adj, pairedHi - adj );
  1010. unpairedHi = Math.max( 10, unpairedHi + adj );
  1011. pairedHi = pairedHi - adj;
  1012. if( unpairedHi <= 10 ){
  1013. this.hideUnpaired();
  1014. return false;
  1015. } else if( !$unpaired.is( 'visible' ) ){
  1016. $unpaired.show();
  1017. //this.$( '.unpaired-filter' ).show();
  1018. }
  1019. if( pairedHi <= 15 ){
  1020. this.hidePaired();
  1021. if( pairedHi < 10 ){ return false; }
  1022. } else if( !$paired.is( 'visible' ) ){
  1023. $paired.show();
  1024. }
  1025. $unpaired.css({
  1026. height : unpairedHi + 'px',
  1027. flex : '0 0 auto'
  1028. });
  1029. return true;
  1030. },
  1031. // ........................................................................ paired
  1032. /** rename a pair when the pair name is clicked */
  1033. _clickPairName : function( ev ){
  1034. var $control = $( ev.currentTarget ),
  1035. pair = this.paired[ $control.parent().index() ],
  1036. response = prompt( 'Enter a new name for the pair:', pair.name );
  1037. if( response ){
  1038. pair.name = response;
  1039. // set a flag (which won't be passed in json creation) for manual naming so we don't overwrite these
  1040. // when adding/removing extensions
  1041. //TODO: kinda hacky
  1042. pair.customizedName = true;
  1043. $control.text( pair.name );
  1044. }
  1045. },
  1046. /** unpair this pair */
  1047. _clickUnpair : function( ev ){
  1048. //if( !ev.currentTarget ){ return true; }
  1049. //TODO: this is a hack bc each paired rev now has two elems (dataset, button)
  1050. var pairIndex = Math.floor( $( ev.currentTarget ).index() / 2 );
  1051. //console.debug( 'pair:', pairIndex );
  1052. //TODO: animate
  1053. this._unpair( this.paired[ pairIndex ] );
  1054. },
  1055. //TODO: the following is ridiculous
  1056. _hoverPaired : function( ev ){
  1057. var $target = $( ev.currentTarget ),
  1058. index = $target.index();
  1059. //TODO: this is a hack bc each paired rev now has two elems (dataset, button)
  1060. if( $target.parents( '.reverse-column' ).size() ){
  1061. index = Math.floor( index / 2 );
  1062. }
  1063. //console.debug( 'index:', index );
  1064. this.emphasizePair( index );
  1065. },
  1066. _hoverOutPaired : function( ev ){
  1067. var $target = $( ev.currentTarget ),
  1068. index = $target.index();
  1069. //TODO: this is a hack bc each paired rev now has two elems (dataset, button)
  1070. if( $target.parents( '.reverse-column' ).size() ){
  1071. index = Math.floor( index / 2 );
  1072. }
  1073. //console.debug( 'index:', index );
  1074. this.deemphasizePair( index );
  1075. },
  1076. emphasizePair : function( index ){
  1077. //console.debug( 'emphasizePairedBorder:', index );
  1078. this.$( '.paired-columns .forward-column .dataset.paired' ).eq( index )
  1079. .add( this.$( '.paired-columns .paired-column .dataset.paired' ).eq( index ) )
  1080. .add( this.$( '.paired-columns .reverse-column .dataset.paired' ).eq( index ) )
  1081. .addClass( 'emphasized' );
  1082. },
  1083. deemphasizePair : function( index ){
  1084. //console.debug( 'deemphasizePairedBorder:', index );
  1085. this.$( '.paired-columns .forward-column .dataset.paired' ).eq( index )
  1086. .add( this.$( '.paired-columns .paired-column .dataset.paired' ).eq( index ) )
  1087. .add( this.$( '.paired-columns .reverse-column .dataset.paired' ).eq( index ) )
  1088. .removeClass( 'emphasized' );
  1089. },
  1090. // ........................................................................ footer
  1091. toggleExtensions : function( force ){
  1092. var creator = this;
  1093. creator.removeExtensions = ( force !== undefined )?( force ):( !creator.removeExtensions );
  1094. _.each( creator.paired, function( pair ){
  1095. // don't overwrite custom names
  1096. if( pair.customizedName ){ return; }
  1097. pair.name = creator._guessNameForPair( pair.forward, pair.reverse );
  1098. });

Large files files are truncated, but you can click here to view the full file