PageRenderTime 65ms CodeModel.GetById 24ms RepoModel.GetById 1ms 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

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. 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

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