1 dojo.declare( 'JBrowse.View.TrackList.Faceted', null, 2 /** 3 * @lends JBrowse.View.TrackList.Faceted.prototype 4 */ 5 { 6 7 /** 8 * Track selector with facets and text searching. 9 * @constructs 10 */ 11 constructor: function(args) { 12 dojo.require('dojox.grid.EnhancedGrid'); 13 dojo.require('dojox.grid.enhanced.plugins.IndirectSelection'); 14 dojo.require('dijit.layout.AccordionContainer'); 15 dojo.require('dijit.layout.AccordionPane'); 16 17 this.browser = args.browser; 18 this.tracksActive = {}; 19 this.config = args; 20 21 // construct the discriminator for whether we will display a 22 // facet selector for this facet 23 this._isSelectableFacet = this._coerceFilter( 24 args.selectableFacetFilter 25 // default facet filtering function 26 || function( facetName, store ){ 27 return ( 28 // has an avg bucket size > 1 29 store.getFacetStats( facetName ).avgBucketSize > 1 30 && 31 // and not an ident or label attribute 32 ! dojo.some( store.getLabelAttributes() 33 .concat( store.getIdentityAttributes() ), 34 function(l) {return l == facetName;} 35 ) 36 ); 37 } 38 ); 39 40 // construct a similar discriminator for which columns will be displayed 41 this.displayColumns = args.displayColumns; 42 this._isDisplayableColumn = this._coerceFilter( 43 args.displayColumnFilter || function() { return true; } 44 ); 45 46 // data store that fetches and filters our track metadata 47 this.trackDataStore = args.trackMetaData; 48 49 // subscribe to commands coming from the the controller 50 dojo.subscribe( '/jbrowse/v1/c/tracks/show', 51 dojo.hitch( this, 'setTracksActive' )); 52 // subscribe to commands coming from the the controller 53 dojo.subscribe( '/jbrowse/v1/c/tracks/hide', 54 dojo.hitch( this, 'setTracksInactive' )); 55 56 this.renderInitial(); 57 58 // once its data is loaded and ready 59 this.trackDataStore.onReady( this, function() { 60 61 // render our controls and so forth 62 this.renderSelectors(); 63 64 // connect events so that when a grid row is selected or 65 // deselected (with the checkbox), publish a message 66 // indicating that the user wants that track turned on or 67 // off 68 dojo.connect( this.dataGrid.selection, 'onSelected', this, function(index) { 69 this._ifNotSuppressed( 'selectionEvents', function() { 70 this._suppress( 'gridUpdate', function() { 71 dojo.publish( '/jbrowse/v1/v/tracks/show', [[this.dataGrid.getItem( index ).conf]] ); 72 }); 73 }); 74 75 }); 76 dojo.connect( this.dataGrid.selection, 'onDeselected', this, function(index) { 77 this._ifNotSuppressed( 'selectionEvents', function() { 78 this._suppress( 'gridUpdate', function() { 79 dojo.publish( '/jbrowse/v1/v/tracks/hide', [[this.dataGrid.getItem( index ).conf]] ); 80 }); 81 }); 82 }); 83 }); 84 this.trackDataStore.onReady( this, '_updateFacetCounts' ); // just once at start 85 86 dojo.connect( this.trackDataStore, 'onFetchSuccess', this, '_updateGridSelections' ); 87 dojo.connect( this.trackDataStore, 'onFetchSuccess', this, '_updateMatchCount' ); 88 }, 89 90 /** 91 * Coerces a string or array of strings into a function that, 92 * given a string, returns true if the string matches one of the 93 * given strings. If passed a function, just returns that 94 * function. 95 * @private 96 */ 97 _coerceFilter: function( filter ) { 98 // if we have a non-function filter, coerce to an array, 99 // then convert that array to a function 100 if( typeof filter == 'string' ) 101 filter = [filter]; 102 if( Array.isArray( filter ) ) { 103 filter = function( store, facetName) { 104 return dojo.some( filter, function(fn) { 105 return facetName == fn; 106 }); 107 }; 108 } 109 return filter; 110 }, 111 112 /** 113 * Call the given callback if none of the given event suppression flags are set. 114 * @private 115 */ 116 _ifNotSuppressed: function( suppressFlags, callback ) { 117 if( typeof suppressFlags == 'string') 118 suppressFlags = [suppressFlags]; 119 if( !this.suppress) 120 this.suppress = {}; 121 if( dojo.some( suppressFlags, function(f) {return this.suppress[f];}, this) ) 122 return undefined; 123 return callback.call(this); 124 }, 125 126 /** 127 * Call the given callback while setting the given event suppression flags. 128 * @private 129 */ 130 _suppress: function( suppressFlags, callback ) { 131 if( typeof suppressFlags == 'string') 132 suppressFlags = [suppressFlags]; 133 if( !this.suppress) 134 this.suppress = {}; 135 dojo.forEach( suppressFlags, function(f) {this.suppress[f] = true; }, this); 136 var retval = callback.call( this ); 137 dojo.forEach( suppressFlags, function(f) {this.suppress[f] = false;}, this); 138 return retval; 139 }, 140 141 /** 142 * Call a method of our object such that it cannot call itself 143 * by way of event cycles. 144 * @private 145 */ 146 _suppressRecursion: function( methodName ) { 147 var flag = ['method_'+methodName]; 148 var method = this[methodName]; 149 return this._ifNotSuppressed( flag, function() { this._suppress( flag, method );}); 150 }, 151 152 renderInitial: function() { 153 this.containerElem = dojo.create( 'div', { 154 id: 'faceted_tracksel', 155 style: { 156 left: '-95%', 157 width: '95%', 158 zIndex: 500 159 } 160 }, 161 document.body ); 162 163 // make the tab that turns the selector on and off 164 dojo.create('div', 165 { 166 className: 'faceted_tracksel_on_off tab', 167 innerHTML: '<img src="img/left_arrow.png"><div>Select<br>tracks</div>' 168 }, 169 this.containerElem 170 ); 171 this.mainContainer = new dijit.layout.BorderContainer( 172 { design: 'headline', gutters: false }, 173 dojo.create('div',{ className: 'mainContainer' }, this.containerElem) 174 ); 175 176 177 this.topPane = new dijit.layout.ContentPane( 178 { region: 'top', 179 id: "faceted_tracksel_top", 180 content: '<div class="title">Select Tracks</div> ' 181 + '<div class="topLink" style="cursor: help"><a title="Track selector help">Help</a></div>' 182 }); 183 dojo.query('div.topLink a[title="Track selector help"]',this.topPane.domNode) 184 .forEach(function(helplink){ 185 var helpdialog = new dijit.Dialog({ 186 "class": 'help_dialog', 187 refocus: false, 188 draggable: false, 189 title: 'Track Selection', 190 content: '<div class="main">' 191 + '<p>The JBrowse Faceted Track Selector makes it easy to search through' 192 + ' large numbers of available tracks to find exactly the ones you want.' 193 + ' You can incrementally filter the track display to narrow it down to' 194 + ' those your are interested in. There are two types of filtering available,' 195 + ' which can be used together:' 196 + ' <b>filtering with data fields</b>, and free-form <b>filtering with text</b>.' 197 + '</p>' 198 + ' <dl><dt>Filtering with Data Fields</dt>' 199 + ' <dd>The left column of the display contains the available <b>data fields</b>. Click on the data field name to expand it, and then select one or more values for that field. This narrows the search to display only tracks that have one of those values for that field. You can do this for any number of fields.<dd>' 200 + ' <dt>Filtering with Text</dt>' 201 + ' <dd>Type text in the "Contains text" box to filter for tracks whose data contains that text. If you type multiple words, tracks are filtered such that they must contain all of those words, in any order. Placing "quotation marks" around the text filters for tracks that contain that phrase exactly. All text matching is case insensitive.</dd>' 202 + ' <dt>Activating Tracks</dt>' 203 + " <dd>To activate and deactivate a track, click its check-box in the left-most column. When the box contains a check mark, the track is activated. You can also turn whole groups of tracks on and off using the check-box in the table heading.</dd>" 204 + " </dl>" 205 + "</div>" 206 }); 207 dojo.connect( helplink, 'onclick', this, function(evt) {helpdialog.show(); return false;}); 208 },this); 209 210 this.mainContainer.addChild( this.topPane ); 211 212 // make both buttons toggle this track selector 213 dojo.query( '.faceted_tracksel_on_off' ) 214 .onclick( dojo.hitch( this, 'toggle' )); 215 216 this.centerPane = new dijit.layout.BorderContainer({region: 'center', "class": 'gridPane', gutters: false}); 217 this.mainContainer.addChild( this.centerPane ); 218 var textFilterContainer = this.renderTextFilter(); 219 220 this.busyIndicator = dojo.create( 221 'div', { 222 innerHTML: '<img src="img/spinner.gif">', 223 className: 'busy_indicator' 224 }, this.containerElem ); 225 226 this.centerPane.addChild( 227 new dijit.layout.ContentPane( 228 { region: 'top', 229 "class": 'gridControls', 230 content: [ 231 dojo.create( 'button', { 232 className: 'faceted_tracksel_on_off', 233 innerHTML: '<img src="img/left_arrow.png"> <div>Back to browser</div>', 234 onclick: dojo.hitch( this, 'hide' ) 235 } 236 ), 237 dojo.create( 'button', { 238 className: 'clear_filters', 239 innerHTML:'<img src="img/red_x.png">' 240 + '<div>Clear All Filters</div>', 241 onclick: dojo.hitch( this, function(evt) { 242 this._clearTextFilterControl(); 243 this._clearAllFacetControls(); 244 this._async( function() { 245 this.updateQuery(); 246 this._updateFacetCounts(); 247 },this).call(); 248 }) 249 } 250 ), 251 this.busyIndicator, 252 textFilterContainer, 253 dojo.create('div', { className: 'matching_record_count' }) 254 ] 255 } 256 ) 257 ); 258 259 260 }, 261 renderSelectors: function() { 262 263 // make our main components 264 var facetContainer = this.renderFacetSelectors(); 265 // put them in their places in the overall layout of the track selector 266 facetContainer.set('region','left'); 267 this.mainContainer.addChild( facetContainer ); 268 269 this.dataGrid = this.renderGrid(); 270 this.dataGrid.set('region','center'); 271 this.centerPane.addChild( this.dataGrid ); 272 273 this.mainContainer.startup(); 274 }, 275 276 _async: function( func, scope ) { 277 var that = this; 278 return function() { 279 var args = arguments; 280 var nativeScope = this; 281 that._busy( true ); 282 window.setTimeout( 283 function() { 284 func.apply( scope || nativeScope, args ); 285 that._busy( false ); 286 }, 287 50 288 ); 289 }; 290 }, 291 292 _busy: function( busy ) { 293 this.busyCount = Math.max( 0, (this.busyCount || 0) + ( busy ? 1 : -1 ) ); 294 if( this.busyCount > 0 ) 295 dojo.addClass( this.containerElem, 'busy' ); 296 else 297 dojo.removeClass( this.containerElem, 'busy' ); 298 }, 299 300 renderGrid: function() { 301 var grid = new dojox.grid.EnhancedGrid({ 302 id: 'trackSelectGrid', 303 store: this.trackDataStore, 304 selectable: true, 305 noDataMessage: "No tracks match the filtering criteria.", 306 structure: [ 307 dojo.map( 308 dojo.filter( this.displayColumns || this.trackDataStore.getFacetNames(), 309 dojo.hitch(this, '_isDisplayableColumn') 310 ), 311 function(facetName) { 312 return {'name': this._facetDisplayName(facetName), 'field': facetName, 'width': '100px'}; 313 }, 314 this 315 ) 316 ], 317 plugins: { 318 indirectSelection: { 319 headerSelector: true 320 } 321 } 322 } 323 ); 324 325 this._monkeyPatchGrid( grid ); 326 return grid; 327 }, 328 329 /** 330 * Given a raw facet name, format it for user-facing display. 331 * @private 332 */ 333 _facetDisplayName: function( facetName ) { 334 // make renameFacets if needed 335 this.renameFacets = this.renameFacets || function(){ 336 var rename = this.config.renameFacets || {}; 337 rename.key = rename.key || 'Name'; 338 return rename; 339 }.call(this); 340 341 return this.renameFacets[facetName] || Util.ucFirst( facetName.replace('_',' ') ); 342 }, 343 344 /** 345 * Apply several run-time patches to the dojox.grid.EnhancedGrid 346 * code to fix bugs and customize the behavior in ways that aren't 347 * quite possible using the regular Dojo APIs. 348 * @private 349 */ 350 _monkeyPatchGrid: function( grid ) { 351 352 // 1. monkey-patch the grid's onRowClick handler to not do 353 // anything. without this, clicking on a row selects it, and 354 // deselects everything else, which is quite undesirable. 355 grid.onRowClick = function() {}; 356 357 // 2. monkey-patch the grid's range-selector to refuse to select 358 // if the selection is too big 359 var origSelectRange = grid.selection.selectRange; 360 grid.selection.selectRange = function( inFrom, inTo ) { 361 var selectionLimit = 30; 362 if( inTo - inFrom > selectionLimit ) { 363 alert( "Too many tracks selected, please select fewer than "+selectionLimit+" tracks." ); 364 return undefined; 365 } 366 return origSelectRange.apply( this, arguments ); 367 }; 368 369 // 3. monkey-patch the grid's scrolling handler to fix 370 // http://bugs.dojotoolkit.org/ticket/15343 371 // diff between this and its implementation in dojox.grid._View.js (1.6.1) is only: 372 // if(top !== this.lastTop) ---> if( Math.abs( top - this.lastTop ) > 1 ) 373 grid.views.views[0].doscroll = function(inEvent){ 374 //var s = dojo.marginBox(this.headerContentNode.firstChild); 375 var isLtr = dojo._isBodyLtr(); 376 if(this.firstScroll < 2){ 377 if((!isLtr && this.firstScroll == 1) || (isLtr && this.firstScroll === 0)){ 378 var s = dojo.marginBox(this.headerNodeContainer); 379 if(dojo.isIE){ 380 this.headerNodeContainer.style.width = s.w + this.getScrollbarWidth() + 'px'; 381 }else if(dojo.isMoz){ 382 //TODO currently only for FF, not sure for safari and opera 383 this.headerNodeContainer.style.width = s.w - this.getScrollbarWidth() + 'px'; 384 //this.headerNodeContainer.style.width = s.w + 'px'; 385 //set scroll to right in FF 386 this.scrollboxNode.scrollLeft = isLtr ? 387 this.scrollboxNode.clientWidth - this.scrollboxNode.scrollWidth : 388 this.scrollboxNode.scrollWidth - this.scrollboxNode.clientWidth; 389 } 390 } 391 this.firstScroll++; 392 } 393 this.headerNode.scrollLeft = this.scrollboxNode.scrollLeft; 394 // 'lastTop' is a semaphore to prevent feedback-loop with setScrollTop below 395 var top = this.scrollboxNode.scrollTop; 396 if(Math.abs( top - this.lastTop ) > 1 ){ 397 this.grid.scrollTo(top); 398 } 399 }; 400 }, 401 402 renderTextFilter: function( parent ) { 403 // make the text input for text filtering 404 this.textFilterLabel = dojo.create( 405 'label', 406 { className: 'textFilterControl', 407 innerHTML: 'Contains text ', 408 id: 'tracklist_textfilter', 409 style: {position: 'relative'} 410 }, 411 parent 412 ); 413 this.textFilterInput = dojo.create( 414 'input', 415 { type: 'text', 416 size: 40, 417 disabled: true, // disabled until shown 418 onkeypress: dojo.hitch( this, function(evt) { 419 if( evt.keyCode == dojo.keys.SHIFT || evt.keyCode == dojo.keys.CTRL || evt.keyCode == dojo.keys.ALT ) 420 return; 421 if( this.textFilterTimeout ) 422 window.clearTimeout( this.textFilterTimeout ); 423 this.textFilterTimeout = window.setTimeout( 424 dojo.hitch( this, function() { 425 this._updateTextFilterControl(); 426 this._async( function() { 427 this.updateQuery(); 428 this._updateFacetCounts(); 429 this.textFilterInput.focus(); 430 },this).call(); 431 this.textFilterInput.focus(); 432 }), 433 500 434 ); 435 this._updateTextFilterControl(); 436 437 evt.stopPropagation(); 438 }) 439 }, 440 this.textFilterLabel 441 ); 442 // make a "clear" button for the text filtering input 443 this.textFilterClearButton = dojo.create('img', { 444 src: 'img/red_x.png', 445 className: 'text_filter_clear', 446 onclick: dojo.hitch( this, function() { 447 this._clearTextFilterControl(); 448 this._async( function() { 449 this.updateQuery(); 450 this._updateFacetCounts(); 451 },this).call(); 452 }), 453 style: { 454 position: 'absolute', 455 right: '4px', 456 top: '20%' 457 } 458 }, this.textFilterLabel ); 459 460 return this.textFilterLabel; 461 }, 462 463 /** 464 * Clear the text filter control input. 465 * @private 466 */ 467 _clearTextFilterControl: function() { 468 this.textFilterInput.value = ''; 469 this._updateTextFilterControl(); 470 }, 471 /** 472 * Update the display of the text filter control based on whether 473 * it has any text in it. 474 * @private 475 */ 476 _updateTextFilterControl: function() { 477 if( this.textFilterInput.value.length ) 478 dojo.addClass( this.textFilterLabel, 'selected' ); 479 else 480 dojo.removeClass( this.textFilterLabel, 'selected' ); 481 482 }, 483 484 /** 485 * Create selection boxes for each searchable facet. 486 */ 487 renderFacetSelectors: function() { 488 var container = new dijit.layout.AccordionContainer({style: 'width: 200px'}); 489 490 var store = this.trackDataStore; 491 this.facetSelectors = {}; 492 493 // render a facet selector for a pseudo-facet holding 494 // attributes regarding the tracks the user has been working 495 // with 496 var usageFacet = this._renderFacetSelector( 497 'My Tracks', ['Currently Active', 'Recently Used'] ); 498 usageFacet.set('class', 'myTracks' ); 499 container.addChild( usageFacet ); 500 501 // for the facets from the store, only render facet selectors 502 // for ones that are not identity attributes, and have an 503 // average bucket size greater than 1 504 var selectableFacets = 505 dojo.filter( this.config.selectableFacets || store.getFacetNames(), 506 function( facetName ) { 507 return this._isSelectableFacet( facetName, this.trackDataStore ); 508 }, 509 this 510 ); 511 512 dojo.forEach( selectableFacets, function(facetName) { 513 514 // get the values of this facet 515 var values = store.getFacetValues(facetName).sort(); 516 if( !values || !values.length ) 517 return; 518 519 var facetPane = this._renderFacetSelector( facetName, values ); 520 container.addChild( facetPane ); 521 },this); 522 523 return container; 524 }, 525 526 /** 527 * Make HTML elements for a single facet selector. 528 * @private 529 * @returns {dijit.layout.AccordionPane} 530 */ 531 _renderFacetSelector: function( /**String*/ facetName, /**Array[String]*/ values ) { 532 533 var facetPane = new dijit.layout.AccordionPane( 534 { 535 title: '<div id="facet_title_' + facetName +'" ' 536 + 'class="facetTitle">' 537 + this._facetDisplayName(facetName) 538 + ' <a class="clearFacet"><img src="img/red_x.png" /></a>' 539 + '</div>' 540 }); 541 542 // make a selection control for the values of this facet 543 var facetControl = dojo.create( 'table', {className: 'facetSelect'}, facetPane.containerNode ); 544 // populate selector's options 545 this.facetSelectors[facetName] = dojo.map( 546 values, 547 function(val) { 548 var that = this; 549 var node = dojo.create( 550 'tr', 551 { className: 'facetValue', 552 innerHTML: '<td class="count"></td><td class="value">'+ val + '</td>', 553 onclick: function(evt) { 554 dojo.toggleClass(this, 'selected'); 555 that._updateFacetControl( facetName ); 556 that._async( function() { 557 that.updateQuery(); 558 that._updateFacetCounts( facetName ); 559 }).call(); 560 } 561 }, 562 facetControl 563 ); 564 node.facetValue = val; 565 return node; 566 }, 567 this 568 ); 569 570 return facetPane; 571 }, 572 573 /** 574 * Clear all the selections from all of the facet controls. 575 * @private 576 */ 577 _clearAllFacetControls: function() { 578 dojo.forEach( dojof.keys( this.facetSelectors ), function( facetName ) { 579 this._clearFacetControl( facetName ); 580 },this); 581 }, 582 583 /** 584 * Clear all the selections from the facet control with the given name. 585 * @private 586 */ 587 _clearFacetControl: function( facetName ) { 588 dojo.forEach( this.facetSelectors[facetName] || [], function(selector) { 589 dojo.removeClass(selector,'selected'); 590 },this); 591 this._updateFacetControl( facetName ); 592 }, 593 594 /** 595 * Incrementally update the facet counts as facet values are selected. 596 * @private 597 */ 598 _updateFacetCounts: function( /**String*/ skipFacetName ) { 599 dojo.forEach( dojof.keys( this.facetSelectors ), function( facetName ) { 600 if( facetName == 'My Tracks' )// || facetName == skipFacetName ) 601 return; 602 var thisFacetCounts = this.trackDataStore.getFacetCounts( facetName ); 603 dojo.forEach( this.facetSelectors[facetName] || [], function( selectorNode ) { 604 dojo.query('.count',selectorNode) 605 .forEach( function(countNode) { 606 var count = thisFacetCounts ? thisFacetCounts[ selectorNode.facetValue ] || 0 : 0; 607 countNode.innerHTML = Util.addCommas( count ); 608 if( count ) 609 dojo.removeClass( selectorNode, 'disabled'); 610 else 611 dojo.addClass( selectorNode, 'disabled' ); 612 },this); 613 //dojo.removeClass(selector,'selected'); 614 },this); 615 this._updateFacetControl( facetName ); 616 },this); 617 }, 618 619 /** 620 * Update the title bar of the given facet control to reflect 621 * whether it has selected values in it. 622 */ 623 _updateFacetControl: function( facetName ) { 624 var titleContent = dojo.byId('facet_title_'+facetName); 625 626 // if all our values are disabled, add 'disabled' to our 627 // title's CSS classes 628 if( dojo.every( this.facetSelectors[facetName] ||[], function(sel) { 629 return dojo.hasClass( sel, 'disabled' ); 630 },this) 631 ) { 632 dojo.addClass( titleContent, 'disabled' ); 633 } 634 635 // if we have some selected values, make a "clear" button, and 636 // add 'selected' to our title's CSS classes 637 if( dojo.some( this.facetSelectors[facetName] || [], function(sel) { 638 return dojo.hasClass( sel, 'selected' ); 639 }, this ) ) { 640 var clearFunc = dojo.hitch( this, function(evt) { 641 this._clearFacetControl( facetName ); 642 this._async( function() { 643 this.updateQuery(); 644 this._updateFacetCounts( facetName ); 645 },this).call(); 646 evt.stopPropagation(); 647 }); 648 dojo.addClass( titleContent, 'selected' ); 649 dojo.query( '> a', titleContent ) 650 .forEach(function(node) { node.onclick = clearFunc; },this) 651 .attr('title','clear selections'); 652 } 653 // otherwise, no selected values 654 else { 655 dojo.removeClass( titleContent, 'selected' ); 656 dojo.query( '> a', titleContent ) 657 .onclick( function(){return false;}) 658 .removeAttr('title'); 659 } 660 }, 661 662 /** 663 * Update the query we are using with the track metadata store 664 * based on the values of the search form elements. 665 */ 666 updateQuery: function() { 667 this._suppressRecursion( '_updateQuery' ); 668 }, 669 _updateQuery: function() { 670 var newQuery = {}; 671 672 var is_selected = function(node) { 673 return dojo.hasClass(node,'selected'); 674 }; 675 676 // update from the My Tracks pseudofacet 677 (function() { 678 var mytracks_options = this.facetSelectors['My Tracks']; 679 680 // index the optoins by name 681 var byname = {}; 682 dojo.forEach( mytracks_options, function(opt){ byname[opt.facetValue] = opt;}); 683 684 // if filtering for active tracks, add the labels for the 685 // currently selected tracks to the query 686 if( is_selected( byname['Currently Active'] ) ) { 687 var activeTrackLabels = dojof.keys(this.tracksActive || {}); 688 newQuery.label = Util.uniq( 689 (newQuery.label ||[]) 690 .concat( activeTrackLabels ) 691 ); 692 } 693 694 // if filtering for recently used tracks, add the labels of recently used tracks 695 if( is_selected( byname['Recently Used'])) { 696 var recentlyUsed = dojo.map( 697 this.browser.getRecentlyUsedTracks(), 698 function(t){ 699 return t.label; 700 } 701 ); 702 703 newQuery.label = Util.uniq( 704 (newQuery.label ||[]) 705 .concat(recentlyUsed) 706 ); 707 } 708 709 // finally, if something is selected in here, but we have 710 // not come up with any track labels, then insert a dummy 711 // track label value that will never match, because the 712 // query engine ignores empty arrayrefs. 713 if( ( ! newQuery.label || ! newQuery.label.length ) 714 && dojo.some( mytracks_options, is_selected ) 715 ) { 716 newQuery.label = ['FAKE LABEL THAT IS HIGHLY UNLIKELY TO EVER MATCH ANYTHING']; 717 } 718 719 }).call(this); 720 721 // update from the text filter 722 if( this.textFilterInput.value.length ) { 723 newQuery.text = this.textFilterInput.value; 724 } 725 726 // update from the data-based facet selectors 727 dojo.forEach( this.trackDataStore.getFacetNames(), function(facetName) { 728 var options = this.facetSelectors[facetName]; 729 if( !options ) return; 730 731 var selectedFacets = dojo.map( 732 dojo.filter( options, is_selected ), 733 function(opt) {return opt.facetValue;} 734 ); 735 if( selectedFacets.length ) 736 newQuery[facetName] = selectedFacets; 737 },this); 738 739 this.query = newQuery; 740 this.dataGrid.setQuery( this.query ); 741 this._updateMatchCount(); 742 }, 743 744 /** 745 * Update the match-count text in the grid controls bar based 746 * on the last query that was run against the store. 747 * @private 748 */ 749 _updateMatchCount: function() { 750 var count = this.dataGrid.store.getCount(); 751 dojo.query( '.matching_record_count', this.containerElem ) 752 .forEach( function(n) { 753 n.innerHTML = Util.addCommas(count) + ' matching track' + ( count == 1 ? '' : 's' ); 754 } 755 ); 756 }, 757 758 /** 759 * Update the grid to have only rows checked that correspond to 760 * tracks that are currently active. 761 * @private 762 */ 763 _updateGridSelections: function() { 764 // keep selection events from firing while we mess with the 765 // grid 766 this._ifNotSuppressed('gridUpdate', function(){ 767 this._suppress('selectionEvents', function() { 768 this.dataGrid.selection.deselectAll(); 769 770 // check the boxes that should be checked, based on our 771 // internal memory of what tracks should be on. 772 for( var i= 0; i < Math.min( this.dataGrid.get('rowCount'), this.dataGrid.get('rowsPerPage') ); i++ ) { 773 var item = this.dataGrid.getItem( i ); 774 var label = this.dataGrid.store.getIdentity( item ); 775 if( this.tracksActive[label] ) 776 this.dataGrid.rowSelectCell.toggleRow( i, true ); 777 } 778 779 }); 780 }); 781 }, 782 783 /** 784 * Given an array of track configs, update the track list to show 785 * that they are turned on. 786 */ 787 setTracksActive: function( /**Array[Object]*/ trackConfigs ) { 788 dojo.forEach( trackConfigs, function(conf) { 789 this.tracksActive[conf.label] = true; 790 },this); 791 }, 792 793 /** 794 * Given an array of track configs, update the track list to show 795 * that they are turned off. 796 */ 797 setTracksInactive: function( /**Array[Object]*/ trackConfigs ) { 798 dojo.forEach( trackConfigs, function(conf) { 799 delete this.tracksActive[conf.label]; 800 },this); 801 }, 802 803 /** 804 * Make the track selector visible. 805 */ 806 show: function() { 807 window.setTimeout( dojo.hitch( this, function() { 808 this.textFilterInput.disabled = false; 809 this.textFilterInput.focus(); 810 }), 300); 811 812 dojo.addClass( this.containerElem, 'active' ); 813 dojo.animateProperty({ 814 node: this.containerElem, 815 properties: { 816 left: { start: -95, end: 0, units: '%' } 817 } 818 }).play(); 819 820 this.shown = true; 821 }, 822 823 /** 824 * Make the track selector invisible. 825 */ 826 hide: function() { 827 828 dojo.removeClass( this.containerElem, 'active' ); 829 830 dojo.animateProperty({ 831 node: this.containerElem, 832 properties: { 833 left: { start: 0, end: -95, units: '%' } 834 } 835 }).play(); 836 837 this.textFilterInput.blur(); 838 this.textFilterInput.disabled = true; 839 840 this.shown = false; 841 }, 842 843 /** 844 * Toggle whether the track selector is visible. 845 */ 846 toggle: function() { 847 this.shown ? this.hide() : this.show(); 848 } 849 });