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