1 /* 2 * Copyright (c) 2013-2016 LabKey Corporation 3 * 4 * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 5 */ 6 LABKEY.FilterDialog = Ext.extend(Ext.Window, { 7 8 autoHeight: true, 9 10 bbarCfg : { 11 bodyStyle : 'border-top: 1px solid black;' 12 }, 13 14 cls: 'labkey-filter-dialog', 15 16 closeAction: 'destroy', 17 18 defaults: { 19 border: false, 20 msgTarget: 'under' 21 }, 22 23 itemId: 'filterWindow', 24 25 modal: true, 26 27 resizable: false, 28 29 // 24846 30 width: Ext.isGecko ? 425 : 410, 31 32 allowFacet : undefined, 33 34 cacheFacetResults: true, 35 36 initComponent : function() { 37 38 if (!this['dataRegionName']) { 39 console.error('dataRegionName is required for a LABKEY.FilterDialog'); 40 return; 41 } 42 43 this.column = this.column || this.boundColumn; // backwards compat 44 if (!this.configureColumn(this.column)) { 45 return; 46 } 47 48 Ext.apply(this, { 49 title: this.title || "Show Rows Where " + this.column.caption + "...", 50 51 carryfilter : true, // whether filter state should try to be carried between views (e.g. when changing tabs) 52 53 // buttons 54 buttons: this.configureButtons(), 55 56 // hook key events 57 keys:[{ 58 key: Ext.EventObject.ENTER, 59 handler: this.onApply, 60 scope: this 61 },{ 62 key: Ext.EventObject.ESC, 63 handler: this.closeDialog, 64 scope: this 65 }], 66 67 // listeners 68 listeners: { 69 destroy: function() { 70 if (this.focusTask) { 71 Ext.TaskMgr.stop(this.focusTask); 72 } 73 }, 74 resize : function(panel) { panel.syncShadow(); }, 75 scope : this 76 } 77 }); 78 79 this.items = [this.getContainer()]; 80 81 LABKEY.FilterDialog.superclass.initComponent.call(this); 82 }, 83 84 allowFaceting : function() { 85 if (Ext.isDefined(this.allowFacet)) 86 return this.allowFacet; 87 88 var dr = this.getDataRegion(); 89 if (!this.isQueryDataRegion(dr)) { 90 this.allowFacet = false; 91 return this.allowFacet; 92 } 93 94 this.allowFacet = false; 95 switch (this.column.facetingBehaviorType) { 96 97 case 'ALWAYS_ON': 98 this.allowFacet = true; 99 break; 100 case 'ALWAYS_OFF': 101 this.allowFacet = false; 102 break; 103 case 'AUTOMATIC': 104 // auto rules are if the column is a lookup or dimension 105 // OR if it is of type : (boolean, int, date, text), multiline excluded 106 if (this.column.lookup || this.column.dimension) 107 this.allowFacet = true; 108 else if (this.jsonType == 'boolean' || this.jsonType == 'int' || 109 (this.jsonType == 'string' && this.column.inputType != 'textarea')) 110 this.allowFacet = true; 111 break; 112 } 113 114 return this.allowFacet; 115 }, 116 117 // Returns an Array of button configurations based on supported operations on this column 118 configureButtons : function() { 119 var buttons = [ 120 {text: 'OK', handler: this.onApply, scope: this}, 121 {text: 'CANCEL', handler: this.closeDialog, scope: this} 122 ]; 123 124 if (this.getDataRegion()) { 125 buttons.push({text: 'CLEAR FILTER', handler: this.clearFilter, scope: this}); 126 buttons.push({text: 'CLEAR ALL FILTERS', handler: this.clearAllFilters, scope: this}); 127 } 128 129 return buttons; 130 }, 131 132 // Returns true if the initialization was a success 133 configureColumn : function(column) { 134 if (!column) { 135 console.error('A column is required for LABKEY.FilterDialog'); 136 return false; 137 } 138 139 Ext.apply(this, { 140 // DEPRECATED: Either invoked from GWT, which will handle the commit itself. 141 // Or invoked as part of a regular filter dialog on a grid 142 changeFilterCallback: this.confirmCallback, 143 144 fieldCaption: column.caption, 145 fieldKey: column.lookup && column.displayField ? column.displayField : column.fieldKey, // terrible 146 jsonType: (column.displayFieldJsonType ? column.displayFieldJsonType : column.jsonType) || 'string' 147 }); 148 149 return true; 150 }, 151 152 onApply : function() { 153 if (this.apply()) 154 this.closeDialog(); 155 }, 156 157 // Validates and applies the current filter(s) to the DataRegion 158 apply : function() { 159 var view = this.getContainer().getActiveTab(); 160 var isValid = true; 161 162 if (!view.getForm().isValid()) 163 isValid = false; 164 165 if (isValid) { 166 isValid = view.checkValid(); 167 } 168 169 if (isValid) { 170 171 var dr = this.getDataRegion(), 172 filters = view.getFilters(); 173 174 if (Ext.isFunction(this.changeFilterCallback)) { 175 176 var filterParams = '', sep = ''; 177 for (var f=0; f < filters.length; f++) { 178 filterParams += sep + encodeURIComponent(filters[f].getURLParameterName(this.dataRegionName)) + '=' + encodeURIComponent(filters[f].getURLParameterValue()); 179 sep = '&'; 180 } 181 this.changeFilterCallback.call(this, null, null, filterParams); 182 } 183 else { 184 if (filters.length > 0) { 185 // add the current filter(s) 186 if (view.supportsMultipleFilters) { 187 dr.replaceFilters(filters, this.column); 188 } 189 else 190 dr.replaceFilter(filters[0]); 191 } 192 else { 193 this.clearFilter(); 194 } 195 } 196 } 197 198 return isValid; 199 }, 200 201 clearFilter : function() { 202 var dr = this.getDataRegion(); 203 if (!dr) { return; } 204 Ext.StoreMgr.clear(); 205 dr.clearFilter(this.fieldKey); 206 this.closeDialog(); 207 }, 208 209 clearAllFilters : function() { 210 var dr = this.getDataRegion(); 211 if (!dr) { return; } 212 dr.clearAllFilters(); 213 this.closeDialog(); 214 }, 215 216 closeDialog : function() { 217 this.close(); 218 }, 219 220 getDataRegion : function() { 221 return LABKEY.DataRegions[this.dataRegionName]; 222 }, 223 224 isQueryDataRegion : function(dr) { 225 return dr && dr.schemaName && dr.queryName; 226 }, 227 228 // Returns a class instance of a class that extends Ext.Container. 229 // This container will hold all the views registered to this FilterDialog instance. 230 // For caching purposes assign to this.viewcontainer 231 getContainer : function() { 232 233 if (!this.viewcontainer) { 234 235 var views = this.getViews(); 236 var type = 'TabPanel'; 237 238 if (views.length == 1) { 239 views[0].title = false; 240 type = 'Panel'; 241 } 242 243 var config = { 244 defaults: this.defaults, 245 deferredRender: false, 246 monitorValid: true, 247 248 // sizing and styling 249 autoHeight: true, 250 bodyStyle: 'padding: 5px;', 251 border: true, 252 width: this.width - 5, 253 items: views 254 }; 255 256 if (type == 'TabPanel') { 257 config.listeners = { 258 beforetabchange : function(tp, newTab, oldTab) { 259 if (this.carryfilter && newTab && oldTab && oldTab.isChanged()) { 260 newTab.setFilters(oldTab.getFilters()); 261 } 262 }, 263 tabchange : function() { 264 this.syncShadow(); 265 this.viewcontainer.getActiveTab().doLayout(); // required when facets return while on another tab 266 }, 267 scope : this 268 }; 269 } 270 271 if (views.length > 1) { 272 config.activeTab = (this.allowFaceting() ? 1 : 0); 273 } 274 else { 275 views[0].title = false; 276 } 277 278 this.viewcontainer = new Ext[type](config); 279 280 if (!Ext.isFunction(this.viewcontainer.getActiveTab)) { 281 var me = this; 282 this.viewcontainer.getActiveTab = function() { 283 return me.viewcontainer.items.items[0]; 284 }; 285 // views attempt to hook the 'activate' event but some panel types do not fire 286 // force fire on the first view 287 this.viewcontainer.items.items[0].on('afterlayout', function(p) { 288 p.fireEvent('activate', p); 289 }, this, {single: true}); 290 } 291 } 292 293 return this.viewcontainer; 294 }, 295 296 _getFilters : function() { 297 var filters = []; 298 299 var dr = this.getDataRegion(); 300 if (dr) { 301 Ext.each(dr.getUserFilterArray(), function(ff) { 302 if (this.column.lookup && this.column.displayField && ff.getColumnName() == this.column.displayField) { 303 filters.push(ff); 304 } 305 else if (this.column.fieldKey && ff.getColumnName() == this.column.fieldKey) { 306 filters.push(ff); 307 } 308 }, this); 309 } 310 else if (this.queryString) { // deprecated 311 filters = LABKEY.Filter.getFiltersFromUrl(this.queryString, this.dataRegionName); 312 } 313 314 return filters; 315 }, 316 317 // Override to return your own filter views 318 getViews : function() { 319 320 var filters = this._getFilters(), views = []; 321 322 // default view 323 views.push({ 324 xtype: 'filter-view-default', 325 column: this.column, 326 fieldKey: this.fieldKey, // should not have to hand this in bc the column should supply correctly 327 dataRegionName: this.dataRegionName, 328 jsonType : this.jsonType, 329 filters: filters 330 }); 331 332 // facet view 333 if (this.allowFaceting()) { 334 views.push({ 335 xtype: 'filter-view-faceted', 336 column: this.column, 337 fieldKey: this.fieldKey, // should not have to hand this in bc the column should supply correctly 338 dataRegionName: this.dataRegionName, 339 jsonType : this.jsonType, 340 filters: filters, 341 cacheResults: this.cacheFacetResults, 342 listeners: { 343 invalidfilter : function() { 344 this.carryfilter = false; 345 this.getContainer().setActiveTab(0); 346 this.getContainer().getActiveTab().doLayout(); 347 this.carryfilter = true; 348 }, 349 scope: this 350 }, 351 scope: this 352 }) 353 } 354 355 return views; 356 } 357 }); 358 359 LABKEY.FilterDialog.ViewPanel = Ext.extend(Ext.form.FormPanel, { 360 361 supportsMultipleFilters: false, 362 363 filters : [], 364 365 changed : false, 366 367 initComponent : function() { 368 if (!this['dataRegionName']) { 369 console.error('dataRegionName is requied for a LABKEY.FilterDialog.ViewPanel'); 370 return; 371 } 372 LABKEY.FilterDialog.ViewPanel.superclass.initComponent.call(this); 373 }, 374 375 // Override to provide own view validation 376 checkValid : function() { 377 return true; 378 }, 379 380 getDataRegion : function() { 381 return LABKEY.DataRegions[this.dataRegionName]; 382 }, 383 384 getFilters : function() { 385 console.error('All classes which extend LABKEY.FilterDialog.ViewPanel must implement getFilters()'); 386 }, 387 388 setFilters : function(filterArray) { 389 console.error('All classes which extend LABKEY.FilterDialog.ViewPanel must implement setFilters(filterArray)'); 390 }, 391 392 getXtype : function() { 393 switch (this.jsonType) { 394 case "date": 395 return "datefield"; 396 case "int": 397 case "float": 398 return "textfield"; 399 case "boolean": 400 return 'labkey-booleantextfield'; 401 default: 402 return "textfield"; 403 } 404 }, 405 406 // Returns true if a view has been altered since the last time it was activated 407 isChanged : function() { 408 return this.changed; 409 } 410 }); 411 412 Ext.ns('LABKEY.FilterDialog.View'); 413 414 LABKEY.FilterDialog.View.Default = Ext.extend(LABKEY.FilterDialog.ViewPanel, { 415 416 supportsMultipleFilters: true, 417 418 itemDefaults: { 419 border: false, 420 msgTarget: 'under' 421 }, 422 423 initComponent : function() { 424 425 Ext.apply(this, { 426 autoHeight: true, 427 title: this.title === false ? false : 'Choose Filters', 428 bodyStyle: 'padding: 5px;', 429 bubbleEvents: ['add', 'remove', 'clientvalidation'], 430 defaults: { border: false }, 431 items: this.generateFilterDisplays(2) 432 }); 433 434 this.combos = []; 435 this.inputs = []; 436 437 LABKEY.FilterDialog.View.Default.superclass.initComponent.call(this); 438 439 this.on('activate', this.onViewReady, this, {single: true}); 440 }, 441 442 onViewReady : function() { 443 if (this.filters.length == 0) { 444 for (var c=0; c < this.combos.length; c++) { 445 // Update the input enabled/disabled status by using the 'select' event listener on the combobox. 446 // However, ComboBox doesn't fire 'select' event when changed programatically so we fire it manually. 447 this.combos[c].reset(); 448 this.combos[c].fireEvent('select', this.combos[c], null); 449 this.inputs[c].reset(); 450 } 451 } 452 else { 453 for (var f=0; f < this.filters.length; f++) { 454 if (f < this.combos.length) { 455 var filter = this.filters[f]; 456 var combo = this.combos[f]; 457 458 // Update the input enabled/disabled status by using the 'select' event listener on the combobox. 459 // However, ComboBox doesn't fire 'select' event when changed programatically so we fire it manually. 460 var store = combo.getStore(); 461 if (store) { 462 var rec = store.getAt(store.find('value', filter.getFilterType().getURLSuffix())); 463 if (rec) { 464 combo.setValue(filter.getFilterType().getURLSuffix()); 465 combo.fireEvent('select', combo, rec); 466 } 467 } 468 469 this.inputs[f].setValue(filter.getValue()); 470 } 471 } 472 } 473 474 //Issue 24550: always select the first filter field, and also select text if present 475 if (this.inputs[0]) { 476 this.inputs[0].focus(true, 100, this.inputs[0]); 477 } 478 479 this.changed = false; 480 }, 481 482 checkValid : function() { 483 var combos = this.combos; 484 var inputs = this.inputs, input, value, f; 485 486 var isValid = true; 487 488 Ext.each(combos, function(c, i) { 489 if (!c.isValid()) { 490 isValid = false; 491 } 492 else { 493 input = inputs[i]; 494 value = input.getValue(); 495 496 f = LABKEY.Filter.getFilterTypeForURLSuffix(c.getValue()); 497 498 if (!f) { 499 alert('filter not found: ' + c.getValue()); 500 return; 501 } 502 503 if (f.isDataValueRequired() && Ext.isEmpty(value)) { 504 input.markInvalid('You must enter a value'); 505 isValid = false; 506 } 507 } 508 }); 509 510 return isValid; 511 }, 512 513 inputFieldValidator : function(input, combo) { 514 515 var store = combo.getStore(); 516 if (store) { 517 var rec = store.getAt(store.find('value', combo.getValue())); 518 var filter = LABKEY.Filter.getFilterTypeForURLSuffix(combo.getValue()); 519 520 if (rec) { 521 if (filter.isMultiValued()) 522 return this.validateMultiValueInput(input.getValue(), filter.getMultiValueSeparator(), filter.getMultiValueMinOccurs(), filter.getMultiValueMaxOccurs()); 523 return this.validateInputField(input.getValue()); 524 } 525 } 526 return true; 527 }, 528 529 generateFilterDisplays : function(quantity) { 530 var idx = this.nextIndex(), items = [], i=0; 531 532 for(; i < quantity; i++) { 533 items.push({ 534 xtype: 'panel', 535 layout: 'form', 536 itemId: 'filterPair' + idx, 537 border: false, 538 defaults: this.itemDefaults, 539 items: [this.getComboConfig(idx), this.getInputConfig(idx)], 540 scope: this 541 }); 542 idx++; 543 } 544 return items; 545 }, 546 547 getComboConfig : function(idx) { 548 549 var val = idx === 0 ? LABKEY.Filter.getDefaultFilterForType(this.jsonType).getURLSuffix() : ''; 550 return { 551 xtype: 'combo', 552 itemId: 'filterComboBox' + idx, 553 filterIndex: idx, 554 name: 'filterType_'+(idx + 1), //for compatibility with tests... 555 listWidth: (this.jsonType == 'date' || this.jsonType == 'boolean') ? null : 380, 556 emptyText: idx === 0 ? 'Choose a filter:' : 'No other filter', 557 autoSelect: false, 558 width: 250, 559 minListWidth: 250, 560 triggerAction: 'all', 561 fieldLabel: (idx === 0 ?'Filter Type' : 'and'), 562 store: this.getSelectionStore(idx), 563 displayField: 'text', 564 valueField: 'value', 565 typeAhead: 'false', 566 forceSelection: true, 567 mode: 'local', 568 clearFilterOnReset: false, 569 editable: false, 570 value: val, 571 originalValue: val, 572 listeners : { 573 render : function(combo) { 574 this.combos.push(combo); 575 // Update the associated inputField's enabled/disabled state on initial render 576 this.enableInputField(combo); 577 }, 578 select : function (combo) { 579 this.changed = true; 580 this.enableInputField(combo); 581 }, 582 scope: this 583 }, 584 scope: this 585 }; 586 }, 587 588 enableInputField : function (combo) { 589 590 var idx = combo.filterIndex; 591 var inputField = this.find('itemId', 'inputField'+idx)[0]; 592 593 var filter = LABKEY.Filter.getFilterTypeForURLSuffix(combo.getValue()); 594 var selectedValue = filter ? filter.getURLSuffix() : ''; 595 596 var combos = this.combos; 597 var inputFields = this.inputs; 598 599 if (filter && !filter.isDataValueRequired()) { 600 //Disable the field and allow it to be blank for values 'isblank' and 'isnonblank'. 601 inputField.disable(); 602 inputField.setValue(); 603 } 604 else { 605 inputField.enable(); 606 inputField.validate(); 607 inputField.focus('', 50) 608 } 609 610 //if the value is null, this indicates no filter chosen. if it lacks an operator (ie. isBlank) 611 //in either case, this means we should disable all other filters 612 if(selectedValue == '' || !filter.isDataValueRequired()){ 613 //Disable all subsequent combos 614 Ext.each(combos, function(combo, idx){ 615 //we enable the next combo in the series 616 if(combo.filterIndex == this.filterIndex + 1){ 617 combo.setValue(); 618 inputFields[idx].setValue(); 619 inputFields[idx].enable(); 620 inputFields[idx].validate(); 621 } 622 else if (combo.filterIndex > this.filterIndex){ 623 combo.setValue(); 624 inputFields[idx].disable(); 625 } 626 627 }, this); 628 } 629 else{ 630 //enable the other filterComboBoxes. 631 Ext.each(combos, function(combo, i) { combo.enable(); }, this); 632 633 if (combos.length) { 634 combos[0].focus('', 50); 635 } 636 } 637 }, 638 639 getFilters : function() { 640 641 var inputs = this.inputs; 642 var combos = this.combos; 643 var value, type, filters = []; 644 645 Ext.each(combos, function(c, i) { 646 if (!inputs[i].disabled || (c.getRawValue() != 'No Other Filter')) { 647 value = inputs[i].getValue(); 648 type = LABKEY.Filter.getFilterTypeForURLSuffix(c.getValue()); 649 650 if (!type) { 651 alert('Filter not found for suffix: ' + c.getValue()); 652 } 653 654 filters.push(LABKEY.Filter.create(this.fieldKey, value, type)); 655 } 656 }, this); 657 658 return filters; 659 }, 660 661 getInputConfig : function(idx) { 662 var me = this; 663 return { 664 xtype : this.getXtype(), 665 itemId : 'inputField' + idx, 666 filterIndex : idx, 667 id : 'value_'+(idx + 1), //for compatibility with tests... 668 width : 250, 669 blankText : 'You must enter a value.', 670 validateOnBlur: true, 671 value : null, 672 altFormats: (this.jsonType == "date" ? LABKEY.Utils.getDateAltFormats() : undefined), 673 validator : function(value) { 674 675 // support for filtering '∞' 676 if (me.jsonType == 'float' && value.indexOf('∞') > -1) { 677 value = value.replace('∞', 'Infinity'); 678 this.setRawValue(value); // does not fire validation 679 } 680 681 var combos = me.combos; 682 if (!combos.length) { 683 return; 684 } 685 686 return me.inputFieldValidator(this, combos[idx]); 687 }, 688 listeners: { 689 disable : function(field){ 690 //Call validate after disable so any pre-existing validation errors go away. 691 if(field.rendered) { 692 field.validate(); 693 } 694 }, 695 focus : function(f) { 696 if (this.focusTask) { 697 Ext.TaskMgr.stop(this.focusTask); 698 } 699 }, 700 render : function(input) { 701 me.inputs.push(input); 702 if (!me.focusReady) { 703 me.focusReady = true; 704 // create a task to set the input focus that will get started after layout is complete, 705 // the task will run for a max of 2000ms but will get stopped when the component receives focus 706 this.focusTask = {interval:150, run: function(){ 707 input.focus(null, 50); 708 Ext.TaskMgr.stop(this.focusTask); 709 }, scope: this, duration: 2000}; 710 } 711 }, 712 change : function(input, newVal, oldVal) { 713 if (oldVal != newVal) { 714 this.changed = true; 715 } 716 }, 717 scope : this 718 }, 719 scope: this 720 }; 721 }, 722 723 getSelectionStore : function(storeNum) { 724 var fields = ['text', 'value', 725 {name: 'isMulti', type: Ext.data.Types.BOOL}, 726 {name: 'isOperatorOnly', type: Ext.data.Types.BOOL} 727 ]; 728 var store = new Ext.data.ArrayStore({ 729 fields: fields, 730 idIndex: 1 731 }); 732 var comboRecord = Ext.data.Record.create(fields); 733 734 var filters = LABKEY.Filter.getFilterTypesForType(this.jsonType, this.column.mvEnabled); 735 for (var i=0; i<filters.length; i++) 736 { 737 var filter = filters[i]; 738 store.add(new comboRecord({ 739 text: filter.getLongDisplayText(), 740 value: filter.getURLSuffix(), 741 isMulti: filter.isMultiValued(), 742 isOperatorOnly: filter.isDataValueRequired() 743 })); 744 } 745 746 if (storeNum > 0) { 747 store.removeAt(0); 748 store.insert(0, new comboRecord({text:'No Other Filter', value: ''})); 749 } 750 751 return store; 752 }, 753 754 setFilters : function(filterArray) { 755 this.filters = filterArray; 756 this.onViewReady(); 757 }, 758 759 nextIndex : function() { 760 return 0; 761 }, 762 763 validateMultiValueInput : function(inputValues, multiValueSeparator, minOccurs, maxOccurs) { 764 // Used when "Equals One Of.." or "Between" is selected. Calls validateInputField on each value entered. 765 var values = inputValues.split(multiValueSeparator); 766 var isValid = ""; 767 for(var i = 0; i < values.length; i++){ 768 isValid = this.validateInputField(values[i]); 769 if(isValid !== true){ 770 return isValid; 771 } 772 } 773 774 if (minOccurs !== undefined && minOccurs > 0) 775 { 776 if (values.length < minOccurs) 777 return "At least " + minOccurs + " '" + multiValueSeparator + "' separated values are required"; 778 } 779 780 if (maxOccurs !== undefined && maxOccurs > 0) 781 { 782 if (values.length > maxOccurs) 783 return "At most " + maxOccurs + " '" + multiValueSeparator + "' separated values are allowed"; 784 } 785 786 //If we make it out of the for loop we had no errors. 787 return true; 788 }, 789 790 // The fact that Ext3 ties validation to the editor is a little funny, 791 // but using this shifts the work to Ext 792 validateInputField : function(value) { 793 var map = { 794 'string': 'STRING', 795 'int': 'INT', 796 'float': 'FLOAT', 797 'date': 'DATE', 798 'boolean': 'BOOL' 799 }; 800 var type = map[this.jsonType]; 801 if (type) { 802 var field = new Ext.data.Field({ 803 type: Ext.data.Types[type], 804 allowDecimals : this.jsonType != "int", //will be ignored by anything besides numberfield 805 useNull: true 806 }); 807 808 var convertedVal = field.convert(value); 809 if (!Ext.isEmpty(value) && value != convertedVal) { 810 return "Invalid value: " + value; 811 } 812 } 813 else { 814 console.log('Unrecognized type: ' + this.jsonType); 815 } 816 817 return true; 818 } 819 }); 820 821 Ext.reg('filter-view-default', LABKEY.FilterDialog.View.Default); 822 823 LABKEY.FilterDialog.View.Faceted = Ext.extend(LABKEY.FilterDialog.ViewPanel, { 824 825 MAX_FILTER_CHOICES: 250, // This is the maximum number of filters that will be requested / shown 826 827 applyContextFilters: true, 828 829 /** 830 * Logically convert filters to try and optimize the query on the server. 831 * (e.g. using NOT IN when less than half the available values are checked) 832 */ 833 filterOptimization: true, 834 835 cacheResults: true, 836 837 emptyDisplayValue: '[Blank]', 838 839 initComponent : function() { 840 841 Ext.apply(this, { 842 title : 'Choose Values', 843 border : false, 844 height : 200, 845 bodyStyle: 'overflow-x: hidden; overflow-y: auto', 846 bubbleEvents: ['add', 'remove', 'clientvalidation'], 847 defaults : { 848 border : false 849 }, 850 markDisabled : true, 851 items: [{ 852 layout: 'hbox', 853 style: 'padding-bottom: 5px; overflow-x: hidden', 854 width: 100, 855 defaults: { 856 border: false 857 }, 858 items: [] 859 }] 860 }); 861 862 LABKEY.FilterDialog.View.Faceted.superclass.initComponent.call(this); 863 864 this.on('render', this.onPanelRender, this, {single: true}); 865 }, 866 867 formatValue : function(val) { 868 if(this.column) { 869 if (this.column.extFormatFn) { 870 try { 871 this.column.extFormatFn = eval(this.column.extFormatFn); 872 } 873 catch (error) { 874 console.log('improper extFormatFn: ' + this.column.extFormatFn); 875 } 876 877 if (Ext.isFunction(this.column.extFormatFn)) { 878 val = this.column.extFormatFn(val); 879 } 880 } 881 else if (this.jsonType == 'int') { 882 val = parseInt(val); 883 } 884 } 885 return val; 886 }, 887 888 // copied from Ext 4 Ext.Array.difference 889 difference : function(arrayA, arrayB) { 890 var clone = arrayA.slice(), 891 ln = clone.length, 892 i, j, lnB; 893 894 for (i = 0,lnB = arrayB.length; i < lnB; i++) { 895 for (j = 0; j < ln; j++) { 896 if (clone[j] === arrayB[i]) { 897 clone.splice(j, 1); 898 j--; 899 ln--; 900 } 901 } 902 } 903 904 return clone; 905 }, 906 907 constructFilter : function(selected, unselected) { 908 var filter = null; 909 910 if (selected.length > 0) { 911 912 var columnName = this.fieldKey; 913 914 // one selection 915 if (selected.length == 1) { 916 if (selected[0].get('displayValue') == this.emptyDisplayValue) 917 filter = LABKEY.Filter.create(columnName, null, LABKEY.Filter.Types.ISBLANK); 918 else 919 filter = LABKEY.Filter.create(columnName, selected[0].get('value')); // default EQUAL 920 } 921 else if (this.filterOptimization && selected.length > unselected.length) { 922 // Do the negation 923 if (unselected.length == 1) { 924 var val = unselected[0].get('value'); 925 var type = (val === "" ? LABKEY.Filter.Types.NONBLANK : LABKEY.Filter.Types.NOT_EQUAL_OR_MISSING); 926 927 // 18716: Check if 'unselected' contains empty value 928 filter = LABKEY.Filter.create(columnName, val, type); 929 } 930 else 931 filter = LABKEY.Filter.create(columnName, this.delimitValues(unselected), LABKEY.Filter.Types.NOT_IN); 932 } 933 else { 934 filter = LABKEY.Filter.create(columnName, this.delimitValues(selected), LABKEY.Filter.Types.IN); 935 } 936 } 937 938 return filter; 939 }, 940 941 delimitValues : function(valueArray) { 942 var value = '', sep = ''; 943 for (var s=0; s < valueArray.length; s++) { 944 value += sep + valueArray[s].get('value'); 945 sep = ';'; 946 } 947 return value; 948 }, 949 950 // Implement interface LABKEY.FilterDialog.ViewPanel 951 getFilters : function() { 952 var grid = Ext.getCmp(this.gridID); 953 var filters = []; 954 955 if (grid) { 956 var store = grid.store; 957 var count = store.getCount(); // TODO: Check if store loaded 958 var selected = grid.getSelectionModel().getSelections(); 959 960 if (count == 0 || selected.length == 0 || selected.length == count) { 961 filters = []; 962 } 963 else { 964 var unselected = this.filterOptimization ? this.difference(store.getRange(), selected) : []; 965 filters = [this.constructFilter(selected, unselected)]; 966 } 967 } 968 969 return filters; 970 }, 971 972 // Implement interface LABKEY.FilterDialog.ViewPanel 973 setFilters : function(filterArray) { 974 if (Ext.isArray(filterArray)) { 975 this.filters = filterArray; 976 this.onViewReady(); 977 } 978 }, 979 980 getGridConfig : function(idx) { 981 var sm = new Ext.grid.CheckboxSelectionModel({ 982 listeners: { 983 selectionchange: { 984 fn: function(sm) { 985 // NOTE: this will manually set the checked state of the header checkbox. it would be better 986 // to make this a real tri-state (ie. selecting some records is different then none), but since this is still Ext3 987 // and ext4 will be quite different it doesnt seem worth the effort right now 988 var selections = sm.getSelections(); 989 var headerCell = Ext.fly(sm.grid.getView().getHeaderCell(0)).first('div'); 990 if(selections.length == sm.grid.store.getCount()){ 991 headerCell.addClass('x-grid3-hd-checker-on'); 992 } 993 else { 994 headerCell.removeClass('x-grid3-hd-checker-on'); 995 } 996 997 998 }, 999 buffer: 50 1000 } 1001 } 1002 }); 1003 1004 this.gridID = Ext.id(); 1005 var me = this; 1006 1007 return { 1008 xtype: 'grid', 1009 id: this.gridID, 1010 border: true, 1011 bodyBorder: true, 1012 frame: false, 1013 autoHeight: true, 1014 itemId: 'inputField' + (idx || 0), 1015 filterIndex: idx || 0, 1016 msgTarget: 'title', 1017 store: this.getLookupStore(), 1018 headerClick: false, 1019 viewConfig: { 1020 headerTpl: new Ext.Template( 1021 '<table border="0" cellspacing="0" cellpadding="0" style="{tstyle}">', 1022 '<thead>', 1023 '<tr class="x-grid3-row-table">{cells}</tr>', 1024 '</thead>', 1025 '</table>' 1026 ) 1027 }, 1028 sm: sm, 1029 cls: 'x-grid-noborder', 1030 columns: [ 1031 sm, 1032 new Ext.grid.TemplateColumn({ 1033 header: '<a href="javascript:void(0);">[All]</a>', 1034 dataIndex: 'value', 1035 menuDisabled: true, 1036 resizable: false, 1037 width: 340, 1038 tpl: new Ext.XTemplate('<tpl for=".">' + 1039 '<span class="labkey-link" ext:qtip="Click the label to select only this row. ' + 1040 'Click the checkbox to toggle this row and preserve other selections.">' + 1041 '{[Ext.util.Format.htmlEncode(values["displayValue"])]}' + 1042 '</span></tpl>') 1043 }) 1044 ], 1045 listeners: { 1046 afterrender : function(grid) { 1047 grid.getSelectionModel().on('selectionchange', function() { 1048 this.changed = true; 1049 }, this); 1050 1051 grid.on('viewready', function(g) { 1052 this.gridReady = true; 1053 this.onViewReady(); 1054 }, this, {single: true}); 1055 }, 1056 scope : this 1057 }, 1058 // extend toggle behavior to the header cell, not just the checkbox next to it 1059 onHeaderCellClick : function() { 1060 var sm = this.getSelectionModel(); 1061 var selected = sm.getSelections(); 1062 selected.length == this.store.getCount() ? this.selectNone() : this.selectAll(); 1063 }, 1064 getValue : function() { 1065 var vals = this.getValues(); 1066 if (vals.length == vals.max) { 1067 return []; 1068 } 1069 return vals.values; 1070 }, 1071 getValues : function() { 1072 var values = [], 1073 sels = this.getSelectionModel().getSelections(); 1074 1075 Ext.each(sels, function(rec){ 1076 values.push(rec.get('strValue')); 1077 }, this); 1078 1079 if(values.indexOf('') != -1 && values.length == 1) 1080 values.push(''); //account for null-only filtering 1081 1082 return { 1083 values : values.join(';'), 1084 length : values.length, 1085 max : this.getStore().getCount() 1086 }; 1087 }, 1088 setValue : function(values, negated) { 1089 if (!this.rendered) { 1090 this.on('render', function() { 1091 this.setValue(values, negated); 1092 }, this, {single: true}); 1093 } 1094 1095 if (!Ext.isArray(values)) { 1096 values = values.split(';'); 1097 } 1098 1099 if (this.store.isLoading) { 1100 // need to wait for the store to load to ensure records 1101 this.store.on('load', function() { 1102 this._checkAndLoadValues(values, negated); 1103 }, this, {single: true}); 1104 } 1105 else { 1106 this._checkAndLoadValues(values, negated); 1107 } 1108 }, 1109 _checkAndLoadValues : function(values, negated) { 1110 var records = [], 1111 recIdx, 1112 recordNotFound = false; 1113 1114 Ext.each(values, function(val) { 1115 recIdx = this.store.findBy(function(rec){ 1116 return rec.get('strValue') === val; 1117 }); 1118 1119 if (recIdx != -1) { 1120 records.push(recIdx); 1121 } 1122 else { 1123 // Issue 14710: if the record isnt found, we wont be able to select it, so should reject. 1124 // If it's null/empty, ignore silently 1125 if (!Ext.isEmpty(val)) { 1126 recordNotFound = true; 1127 return false; 1128 } 1129 } 1130 }, this); 1131 1132 if (negated) { 1133 var count = this.store.getCount(), found = false, negRecords = []; 1134 for (var i=0; i < count; i++) { 1135 found = false; 1136 for (var j=0; j < records.length; j++) { 1137 if (records[j] == i) 1138 found = true; 1139 } 1140 if (!found) { 1141 negRecords.push(i); 1142 } 1143 } 1144 records = negRecords; 1145 } 1146 1147 if (recordNotFound) { 1148 // cannot find any matching records 1149 if (me.column.facetingBehaviorType != 'ALWAYS_ON') 1150 me.fireEvent('invalidfilter'); 1151 return; 1152 } 1153 1154 this.getSelectionModel().selectRows(records); 1155 }, 1156 selectAll : function() { 1157 if (this.rendered) { 1158 var sm = this.getSelectionModel(); 1159 sm.selectAll.defer(10, sm); 1160 } 1161 else { 1162 this.on('render', this.selectAll, this, {single: true}); 1163 } 1164 }, 1165 selectNone : function() { 1166 if (this.rendered) { 1167 this.getSelectionModel().selectRows([]); 1168 } 1169 else { 1170 this.on('render', this.selectNone, this, {single: true}); 1171 } 1172 }, 1173 determineNegation: function(filter) { 1174 var suffix = filter.getFilterType().getURLSuffix(); 1175 var negated = suffix == 'neqornull' || suffix == 'notin'; 1176 1177 // negation of the null case is a bit different so check it as a special case. 1178 var value = filter.getURLParameterValue(); 1179 if (value == "" && suffix != 'isblank') { 1180 negated = true; 1181 } 1182 return negated; 1183 }, 1184 selectFilter : function(filter) { 1185 var negated = this.determineNegation(filter); 1186 1187 this.setValue(filter.getURLParameterValue(), negated); 1188 1189 if (!me.filterOptimization && negated) { 1190 me.fireEvent('invalidfilter'); 1191 } 1192 }, 1193 scope : this 1194 }; 1195 }, 1196 1197 onViewReady : function() { 1198 if (this.gridReady && this.storeReady) { 1199 var grid = Ext.getCmp(this.gridID); 1200 this.hideMask(); 1201 1202 if (grid) { 1203 1204 var numFilters = this.filters.length; 1205 var numFacets = grid.store.getCount(); 1206 1207 // apply current filter 1208 if (numFacets == 0) 1209 grid.selectNone(); 1210 else if (numFilters == 0) 1211 grid.selectAll(); 1212 else 1213 grid.selectFilter(this.filters[0]); 1214 1215 if (!grid.headerClick) { 1216 grid.headerClick = true; 1217 var div = Ext.fly(grid.getView().getHeaderCell(1)).first('div'); 1218 div.on('click', grid.onHeaderCellClick, grid); 1219 } 1220 1221 if (numFacets > this.MAX_FILTER_CHOICES) { 1222 this.fireEvent('invalidfilter'); 1223 } 1224 } 1225 } 1226 1227 this.changed = false; 1228 }, 1229 1230 getLookupStore : function() { 1231 var dr = this.getDataRegion(); 1232 var storeId = this.cacheResults ? [dr.schemaName, dr.queryName, this.fieldKey].join('||') : Ext.id(); 1233 1234 // cache 1235 var store = Ext.StoreMgr.get(storeId); 1236 if (store) { 1237 this.storeReady = true; // unsafe 1238 return store; 1239 } 1240 1241 store = new Ext.data.ArrayStore({ 1242 fields : ['value', 'strValue', 'displayValue'], 1243 storeId: storeId 1244 }); 1245 1246 var config = { 1247 schemaName: dr.schemaName, 1248 queryName: dr.queryName, 1249 dataRegionName: dr.name, 1250 viewName: dr.viewName, 1251 column: this.fieldKey, 1252 filterArray: dr.filters, 1253 containerPath: dr.container || dr.containerPath || LABKEY.container.path, 1254 containerFilter: dr.getContainerFilter(), 1255 parameters: dr.getParameters(), 1256 maxRows: this.MAX_FILTER_CHOICES+1, 1257 ignoreFilter: dr.ignoreFilter, 1258 success : function(d) { 1259 if (d && d.values) { 1260 var recs = [], v, i=0, hasBlank = false, isString, formattedValue; 1261 for (; i < d.values.length; i++) { 1262 v = d.values[i]; 1263 formattedValue = this.formatValue(v); 1264 isString = Ext.isString(formattedValue); 1265 1266 if (formattedValue == null || (isString && formattedValue.length == 0) || (!isString && isNaN(formattedValue))) { 1267 hasBlank = true; 1268 } 1269 else if (Ext.isDefined(v)) { 1270 recs.push([v, v.toString(), v.toString()]); 1271 } 1272 } 1273 1274 if (hasBlank) 1275 recs.unshift(['', '', this.emptyDisplayValue]); 1276 1277 store.loadData(recs); 1278 store.isLoading = false; 1279 this.storeReady = true; 1280 this.onViewReady(); 1281 } 1282 }, 1283 scope: this 1284 }; 1285 1286 if (this.applyContextFilters) { 1287 var userFilters = dr.getUserFilterArray(); 1288 if (userFilters && userFilters.length > 0) { 1289 1290 var uf = []; 1291 1292 // Remove filters for the current column 1293 for (var i=0; i < userFilters.length; i++) { 1294 if (userFilters[i].getColumnName() != this.fieldKey) { 1295 uf.push(userFilters[i]); 1296 } 1297 } 1298 1299 config.filterArray = uf; 1300 } 1301 } 1302 1303 // Use Select Distinct 1304 LABKEY.Query.selectDistinctRows(config); 1305 1306 return Ext.StoreMgr.add(store); 1307 }, 1308 1309 onPanelRender : function(panel) { 1310 var toAdd = [{ 1311 xtype: 'panel', 1312 width: this.width - 40, //prevent horizontal scroll 1313 bodyStyle: 'padding-left: 5px;', 1314 items: [ this.getGridConfig(0) ], 1315 listeners : { 1316 afterrender : { 1317 fn: this.showMask, 1318 scope: this, 1319 single: true 1320 } 1321 } 1322 }]; 1323 panel.add(toAdd); 1324 }, 1325 1326 showMask : function() { 1327 if (!this.gridReady && this.getEl()) { 1328 this.getEl().mask('Loading...'); 1329 } 1330 }, 1331 1332 hideMask : function() { 1333 if (this.getEl()) { 1334 this.getEl().unmask(); 1335 } 1336 } 1337 }); 1338 1339 Ext.reg('filter-view-faceted', LABKEY.FilterDialog.View.Faceted); 1340 1341 Ext.ns('LABKEY.ext'); 1342 1343 LABKEY.ext.BooleanTextField = Ext.extend(Ext.form.TextField, { 1344 initComponent : function() { 1345 Ext.apply(this, { 1346 validator: function(val){ 1347 if(!val) 1348 return true; 1349 1350 return LABKEY.Utils.isBoolean(val) ? true : val + " is not a valid boolean. Try true/false; yes/no; on/off; or 1/0."; 1351 } 1352 }); 1353 LABKEY.ext.BooleanTextField.superclass.initComponent.call(this); 1354 } 1355 }); 1356 1357 Ext.reg('labkey-booleantextfield', LABKEY.ext.BooleanTextField); 1358 1359