1 /* 2 * Copyright (c) 2015-2016 LabKey Corporation 3 * 4 * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 5 */ 6 if (!LABKEY.DataRegions) { 7 LABKEY.DataRegions = {}; 8 } 9 10 (function($) { 11 12 // 13 // CONSTANTS 14 // 15 var ALL_FILTERS_SKIP_PREFIX = '.~'; 16 var COLUMNS_PREFIX = '.columns'; 17 var DEFAULT_TIMEOUT = 30000; 18 var PARAM_PREFIX = '.param.'; 19 var REPORTID_PREFIX = '.reportId'; 20 var SORT_PREFIX = '.sort', SORT_ASC = '+', SORT_DESC = '-'; 21 var OFFSET_PREFIX = '.offset'; 22 var MAX_ROWS_PREFIX = '.maxRows', SHOW_ROWS_PREFIX = '.showRows'; 23 var CONTAINER_FILTER_NAME = '.containerFilterName'; 24 var CUSTOM_VIEW_PANELID = '~~customizeView~~'; 25 var VIEWNAME_PREFIX = '.viewName'; 26 27 var VALID_LISTENERS = [ 28 /** 29 * @memberOf LABKEY.DataRegion.prototype 30 * @name afterpanelhide 31 * @event LABKEY.DataRegion.prototype#hidePanel 32 * @description Fires after hiding a visible 'Customize Grid' panel. 33 */ 34 'afterpanelhide', 35 /** 36 * @memberOf LABKEY.DataRegion.prototype 37 * @name afterpanelshow 38 * @event LABKEY.DataRegion.prototype.showPanel 39 * @description Fires after showing 'Customize Grid' panel. 40 */ 41 'afterpanelshow', 42 /** 43 * @memberOf LABKEY.DataRegion.prototype 44 * @name beforechangeview 45 * @event 46 * @description Fires before changing grid/view/report. 47 * @see LABKEY.DataRegion#changeView 48 */ 49 'beforechangeview', 50 /** 51 * @memberOf LABKEY.DataRegion.prototype 52 * @name beforeclearsort 53 * @event 54 * @description Fires before clearing sort applied to grid. 55 * @see LABKEY.DataRegion#clearSort 56 */ 57 'beforeclearsort', 58 /** 59 * @memberOf LABKEY.DataRegion.prototype 60 * @name beforemaxrowschange 61 * @event 62 * @description Fires before change page size. 63 * @see LABKEY.DataRegion#setMaxRows 64 */ 65 'beforemaxrowschange', 66 /** 67 * @memberOf LABKEY.DataRegion.prototype 68 * @name beforeoffsetchange 69 * @event 70 * @description Fires before change page number. 71 * @see LABKEY.DataRegion#setPageOffset 72 */ 73 'beforeoffsetchange', 74 /** 75 * @memberOf LABKEY.DataRegion.prototype 76 * @name beforerefresh 77 * @event 78 * @description Fires before refresh grid. 79 * @see LABKEY.DataRegion#refresh 80 */ 81 'beforerefresh', 82 /** 83 * @memberOf LABKEY.DataRegion.prototype 84 * @name beforesetparameters 85 * @event 86 * @description Fires before setting the parameterized query values for this query. 87 * @see LABKEY.DataRegion#setParameters 88 */ 89 'beforesetparameters', 90 /** 91 * @memberOf LABKEY.DataRegion.prototype 92 * @name beforesortchange 93 * @event 94 * @description Fires before change sorting on the grid. 95 * @see LABKEY.DataRegion#changeSort 96 */ 97 'beforesortchange', 98 /** 99 * @memberOf LABKEY.DataRegion.prototype 100 * @member 101 * @name render 102 * @event 103 * @description Fires when data region renders. 104 */ 105 'render', 106 /** 107 * @memberOf LABKEY.DataRegion.prototype 108 * @name selectchange 109 * @event 110 * @description Fires when data region selection changes. 111 */ 112 'selectchange', 113 /** 114 * @memberOf LABKEY.DataRegion.prototype 115 * @name success 116 * @event 117 * @description Fires when data region loads successfully. 118 */ 119 'success']; 120 121 // TODO: Update constants to not include '.' so mapping can be used easier 122 var REQUIRE_NAME_PREFIX = { 123 '~': true, 124 'columns': true, 125 'param': true, 126 'reportId': true, 127 'sort': true, 128 'offset': true, 129 'maxRows': true, 130 'showRows': true, 131 'containerFilterName': true, 132 'viewName': true, 133 'disableAnalytics': true 134 }; 135 136 // 137 // PRIVATE VARIABLES 138 // 139 var _paneCache = {}; 140 141 /** 142 * The DataRegion constructor is private - to get a LABKEY.DataRegion object, use LABKEY.DataRegions['dataregionname']. 143 * @class LABKEY.DataRegion 144 * The DataRegion class allows you to interact with LabKey grids, including querying and modifying selection state, filters, and more. 145 * @constructor 146 */ 147 LABKEY.DataRegion = function(config) { 148 this._init(config, true); 149 }; 150 151 LABKEY.DataRegion.prototype.toJSON = function() { 152 return { 153 name: this.name, 154 schemaName: this.schemaName, 155 queryName: this.queryName, 156 viewName: this.viewName, 157 offset: this.offset, 158 maxRows: this.maxRows, 159 messages: this.msgbox.toJSON() // hmm, unsure exactly how this works 160 }; 161 }; 162 163 /** 164 * 165 * @param {Object} config 166 * @param {Boolean} [applyDefaults=false] 167 * @private 168 */ 169 LABKEY.DataRegion.prototype._init = function(config, applyDefaults) { 170 171 // ensure name 172 if (!config.dataRegionName) { 173 if (!config.name) { 174 this.name = LABKEY.Utils.id('aqwp'); 175 } 176 else { 177 this.name = config.name; 178 } 179 } 180 else if (!config.name) { 181 this.name = config.dataRegionName; 182 } 183 else { 184 this.name = config.name; 185 } 186 187 if (!this.name) { 188 throw '"name" is required to initialize a LABKEY.DataRegion'; 189 } 190 191 // _useQWPDefaults is only used on initial construction 192 var isQWP = config._useQWPDefaults === true; 193 delete config._useQWPDefaults; 194 195 var settings; 196 197 if (applyDefaults) { 198 199 // defensively remove, not allowed to be set 200 delete config._userSort; 201 202 /** 203 * Config Options 204 */ 205 var defaults = { 206 207 _allowHeaderLock: isQWP, 208 209 _failure: isQWP ? LABKEY.Utils.getOnFailure(config) : undefined, 210 211 _success: isQWP ? LABKEY.Utils.getOnSuccess(config) : undefined, 212 213 aggregates: undefined, 214 215 allowChooseQuery: undefined, 216 217 allowChooseView: undefined, 218 219 async: isQWP, 220 221 bodyClass: undefined, 222 223 buttonBar: undefined, 224 225 buttonBarPosition: undefined, 226 227 chartWizardURL: undefined, 228 229 /** 230 * All rows visible on the current page. 231 */ 232 complete: false, 233 234 /** 235 * The currently applied container filter. Note, this is only if it is set on the URL, otherwise 236 * the containerFilter could come from the view configuration. Use getContainerFilter() 237 * on this object to get the right value. 238 */ 239 containerFilter: undefined, 240 241 containerPath: undefined, 242 243 /** 244 * @deprecated use region.name instead 245 */ 246 dataRegionName: this.name, 247 248 detailsURL: undefined, 249 250 domId: undefined, 251 252 /** 253 * The faceted filter pane as been loaded 254 * @private 255 */ 256 facetLoaded: false, 257 258 filters: undefined, 259 260 frame: isQWP ? undefined : 'none', 261 262 errorType: 'html', 263 264 /** 265 * Id of the DataRegion. Same as name property. 266 */ 267 id: this.name, 268 269 deleteURL: undefined, 270 271 importURL: undefined, 272 273 insertURL: undefined, 274 275 linkTarget: undefined, 276 277 /** 278 * Maximum number of rows to be displayed. 0 if the count is not limited. Read-only. 279 */ 280 maxRows: 0, 281 282 metadata: undefined, 283 284 /** 285 * Name of the DataRegion. Should be unique within a given page. Read-only. This will also be used as the id. 286 */ 287 name: this.name, 288 289 /** 290 * The index of the first row to return from the server (defaults to 0). Use this along with the maxRows config property to request pages of data. 291 */ 292 offset: 0, 293 294 parameters: undefined, 295 296 /** 297 * Name of the query to which this DataRegion is bound. Read-only. 298 */ 299 queryName: '', 300 301 disableAnalytics: false, 302 303 removeableContainerFilter: undefined, 304 305 removeableFilters: undefined, 306 307 removeableSort: undefined, 308 309 renderTo: undefined, 310 311 reportId: undefined, 312 313 requestURL: isQWP ? window.location.href : (document.location.search.substring(1) /* strip the ? */ || ''), 314 315 returnURL: isQWP ? window.location.href : undefined, 316 317 /** 318 * Schema name of the query to which this DataRegion is bound. Read-only. 319 */ 320 schemaName: '', 321 322 /** 323 * An object to use as the callback function's scope. Defaults to this. 324 */ 325 scope: this, 326 327 /** 328 * URL to use when selecting all rows in the grid. May be null. Read-only. 329 */ 330 selectAllURL: undefined, 331 332 selectedCount: 0, 333 334 shadeAlternatingRows: undefined, 335 336 showBorders: undefined, 337 338 showDeleteButton: undefined, 339 340 showDetailsColumn: undefined, 341 342 showExportButtons: undefined, 343 344 showInsertNewButton: undefined, 345 346 showPagination: undefined, 347 348 showRecordSelectors: false, 349 350 showReports: undefined, 351 352 /** 353 * An enum declaring which set of rows to show. all | selected | unselected | paginated 354 */ 355 showRows: 'paginated', 356 357 showSurroundingBorder: undefined, 358 359 showUpdateColumn: undefined, 360 361 /** 362 * Open the customize view panel after rendering. The value of this option can be "true" or one of "ColumnsTab", "FilterTab", or "SortTab". 363 */ 364 showViewPanel: undefined, 365 366 sort: undefined, 367 368 sql: undefined, 369 370 /** 371 * If true, no alert will appear if there is a problem rendering the QueryWebpart. This is most often encountered if page configuration changes between the time when a request was made and the content loads. Defaults to false. 372 */ 373 suppressRenderErrors: false, 374 375 /** 376 * A timeout for the AJAX call, in milliseconds. 377 */ 378 timeout: undefined, 379 380 title: undefined, 381 382 titleHref: undefined, 383 384 totalRows: undefined, // totalRows isn't available when showing all rows. 385 386 updateURL: undefined, 387 388 userContainerFilter: undefined, // TODO: Incorporate this with the standard containerFilter 389 390 userFilters: {}, 391 392 /** 393 * Name of the custom view to which this DataRegion is bound, may be blank. Read-only. 394 */ 395 viewName: null 396 }; 397 398 settings = $.extend({}, defaults, config); 399 } 400 else { 401 settings = $.extend({}, config); 402 } 403 404 // if 'filters' is not specified and 'filterArray' is, use 'filterArray' 405 if (!$.isArray(settings.filters) && $.isArray(config.filterArray)) { 406 settings.filters = config.filterArray; 407 } 408 409 // Any 'key' of this object will not be copied from settings to the region instance 410 var blackList = { 411 failure: true, 412 success: true 413 }; 414 415 for (var s in settings) { 416 if (settings.hasOwnProperty(s) && !blackList[s]) { 417 this[s] = settings[s]; 418 } 419 } 420 421 if (config.renderTo) { 422 _convertRenderTo(this, config.renderTo); 423 } 424 425 if ($.isArray(this.removeableFilters)) { 426 LABKEY.Filter.appendFilterParams(this.userFilters, this.removeableFilters, this.name); 427 delete this.removeableFilters; // they've been applied 428 } 429 430 // initialize sorting 431 if (this._userSort === undefined) { 432 this._userSort = _getUserSort(this, true /* asString */); 433 } 434 435 if (LABKEY.Utils.isString(this.removeableSort)) { 436 this._userSort = this.removeableSort + (this._userSort ? this._userSort : ''); 437 delete this.removeableSort; 438 } 439 440 this._allowHeaderLock = this.allowHeaderLock === true; 441 442 if (!config.messages) { 443 this.messages = {}; 444 } 445 446 /** 447 * @ignore 448 * Non-configurable Options 449 */ 450 this.selectionModified = false; 451 this.panelConfigurations = {}; // formerly, panelButtonContents 452 453 if (isQWP && this.renderTo) { 454 _load(this); 455 } 456 else if (!isQWP) { 457 this._initMessaging(); 458 this._initSelection(); 459 this._initHeaderLocking(); 460 this._initPaging(); 461 this._initCustomViews(); 462 this._initPanes(); 463 } 464 // else the user needs to call render 465 466 // bind supported listeners 467 if (isQWP) { 468 var me = this; 469 if (config.listeners) { 470 var scope = config.listeners.scope || me; 471 $.each(config.listeners, function(event, handler) { 472 if ($.inArray(event, VALID_LISTENERS) > -1) { 473 474 // support either "event: function" or "event: { fn: function }" 475 var callback; 476 if ($.isFunction(handler)) { 477 callback = handler; 478 } 479 else if ($.isFunction(handler.fn)) { 480 callback = handler.fn; 481 } 482 else { 483 throw 'Unsupported listener configuration: ' + event; 484 } 485 486 $(me).bind(event, function() { 487 callback.apply(scope, $(arguments).slice(1)); 488 }); 489 } 490 else if (event != 'scope') { 491 throw 'Unsupported listener: ' + event; 492 } 493 }); 494 } 495 } 496 }; 497 498 LABKEY.DataRegion.prototype.destroy = function() { 499 // currently a no-op, but should be used to clean-up after ourselves 500 this.disableHeaderLock(); 501 }; 502 503 /** 504 * Refreshes the grid, via AJAX region is in async mode (loaded through a QueryWebPart), 505 * and via a page reload otherwise. Can be prevented with a listener 506 * on the 'beforerefresh' 507 * event. 508 */ 509 LABKEY.DataRegion.prototype.refresh = function() { 510 511 //var event = $.Event("beforerefresh"); 512 // 513 //$(this).trigger(event); 514 // 515 //if (event.isDefaultPrevented()) { 516 // return; 517 //} 518 $(this).trigger('beforerefresh', this); 519 520 if (this.async) { 521 _load(this); 522 } 523 else { 524 window.location.reload(); 525 } 526 }; 527 528 // 529 // Filtering 530 // 531 532 /** 533 * Add a filter to this Data Region. 534 * @param {LABKEY.Filter} filter 535 * @see LABKEY.DataRegion.addFilter static method. 536 */ 537 LABKEY.DataRegion.prototype.addFilter = function(filter) { 538 _updateFilter(this, filter); 539 }; 540 541 /** 542 * Removes all filters from the DataRegion 543 */ 544 LABKEY.DataRegion.prototype.clearAllFilters = function() { 545 //var event = $.Event("beforeclearallfilters"); 546 // 547 //$(this).trigger(event, this); 548 // 549 //if (event.isDefaultPrevented()) { 550 // return; 551 //} 552 553 if (this.async) { 554 this.offset = 0; 555 this.userFilters = {}; 556 } 557 558 _removeParameters(this, [ALL_FILTERS_SKIP_PREFIX, OFFSET_PREFIX]); 559 }; 560 561 /** 562 * Removes all the filters for a particular field 563 * @param {string|FieldKey} fieldKey the name of the field from which all filters should be removed 564 */ 565 LABKEY.DataRegion.prototype.clearFilter = function(fieldKey) { 566 fieldKey = _resolveFieldKey(this, fieldKey); 567 568 if (fieldKey) { 569 var columnPrefix = '.' + fieldKey.toString() + '~'; 570 571 //var event = $.Event("beforeclearfilter"); 572 // 573 //$(this).trigger(event, this, columnName); 574 // 575 //if (event.isDefaultPrevented()) { 576 // return; 577 //} 578 579 if (this.async) { 580 this.offset = 0; 581 582 if (this.userFilters) { 583 var namePrefix = this.name + columnPrefix, 584 me = this; 585 586 $.each(this.userFilters, function(name, v) { 587 if (name.indexOf(namePrefix) >= 0) { 588 delete me.userFilters[name]; 589 } 590 }); 591 } 592 } 593 594 _removeParameters(this, [columnPrefix, OFFSET_PREFIX]); 595 } 596 }; 597 598 /** 599 * Returns the {@link LABKEY.Query.containerFilter} currently applied to the DataRegion. Defaults to LABKEY.Query.containerFilter.current. 600 * @returns {String} The container filter currently applied to this DataRegion. Defaults to 'undefined' if a container filter is not specified by the configuration. 601 * @see LABKEY.DataRegion#getUserContainerFilter to get the containerFilter value from the URL. 602 */ 603 LABKEY.DataRegion.prototype.getContainerFilter = function() { 604 var cf; 605 606 if (LABKEY.Utils.isString(this.containerFilter) && this.containerFilter.length > 0) { 607 cf = this.containerFilter; 608 } 609 else if (LABKEY.Utils.isObject(this.view) && LABKEY.Utils.isString(this.view.containerFilter) && this.view.containerFilter.length > 0) { 610 cf = this.view.containerFilter; 611 } 612 613 return cf; 614 }; 615 616 LABKEY.DataRegion.prototype.getDataRegion = function() { 617 return this; 618 }; 619 620 /** 621 * Returns the user {@link LABKEY.Query.containerFilter} parameter from the URL. 622 * @returns {LABKEY.Query.containerFilter} The user container filter. 623 */ 624 LABKEY.DataRegion.prototype.getUserContainerFilter = function() { 625 return this.getParameter(this.name + CONTAINER_FILTER_NAME); 626 }; 627 628 /** 629 * Returns the user filter from the URL. The filter is represented as an Array of objects of the form: 630 * <ul> 631 * <li><b>fieldKey</b>: {String} The field key of the filter. 632 * <li><b>op</b>: {String} The filter operator (eg. "eq" or "in") 633 * <li><b>value</b>: {String} Optional value to filter by. 634 * </ul> 635 * @returns {Object} Object representing the user filter. 636 * @deprecated 12.2 Use getUserFilterArray instead 637 */ 638 LABKEY.DataRegion.prototype.getUserFilter = function() { 639 640 if (LABKEY.devMode) { 641 console.warn([ 642 'LABKEY.DataRegion.getUserFilter() is deprecated since release 12.2.', 643 'Consider using getUserFilterArray() instead.' 644 ].join(' ')); 645 } 646 647 var userFilter = []; 648 649 $.each(this.getUserFilterArray(), function(i, filter) { 650 userFilter.push({ 651 fieldKey: filter.getColumnName(), 652 op: filter.getFilterType().getURLSuffix(), 653 value: filter.getValue() 654 }); 655 }); 656 657 return userFilter; 658 }; 659 660 /** 661 * Returns an Array of LABKEY.Filter instances constructed from the URL. 662 * @returns {Array} Array of {@link LABKEY.Filter} objects that represent currently applied filters. 663 */ 664 LABKEY.DataRegion.prototype.getUserFilterArray = function() { 665 var userFilter = [], me = this; 666 667 var pairs = _getParameters(this); 668 $.each(pairs, function(i, pair) { 669 if (pair[0].indexOf(me.name + '.') == 0 && pair[0].indexOf('~') > -1) { 670 var tilde = pair[0].indexOf('~'); 671 var fieldKey = pair[0].substring(me.name.length + 1, tilde); 672 var op = pair[0].substring(tilde + 1); 673 userFilter.push(LABKEY.Filter.create(fieldKey, pair[1], LABKEY.Filter.getFilterTypeForURLSuffix(op))); 674 } 675 }); 676 677 return userFilter; 678 }; 679 680 /** 681 * Remove a filter on this DataRegion. 682 * @param {LABKEY.Filter} filter 683 */ 684 LABKEY.DataRegion.prototype.removeFilter = function(filter) { 685 if (LABKEY.Utils.isObject(filter) && LABKEY.Utils.isFunction(filter.getColumnName)) { 686 _updateFilter(this, null, [this.name + '.' + filter.getColumnName() + '~']); 687 } 688 }; 689 690 /** 691 * Replace a filter on this Data Region. Optionally, supply another filter to replace for cases when the filter 692 * columns don't match exactly. 693 * @param {LABKEY.Filter} filter 694 * @param {LABKEY.Filter} [filterToReplace] 695 */ 696 LABKEY.DataRegion.prototype.replaceFilter = function(filter, filterToReplace) { 697 var target = filterToReplace ? filterToReplace : filter; 698 _updateFilter(this, filter, [this.name + '.' + target.getColumnName() + '~']); 699 }; 700 701 /** 702 * @ignore 703 * @param filters 704 * @param columnNames 705 */ 706 LABKEY.DataRegion.prototype.replaceFilters = function(filters, columnNames) { 707 var filterPrefixes = [], 708 filterParams = [], 709 me = this; 710 711 if ($.isArray(filters)) { 712 $.each(filters, function(i, filter) { 713 filterPrefixes.push(me.name + '.' + filter.getColumnName() + '~'); 714 filterParams.push([filter.getURLParameterName(me.name), filter.getURLParameterValue()]); 715 }); 716 } 717 718 var fieldKeys = []; 719 720 if ($.isArray(columnNames)) { 721 fieldKeys = fieldKeys.concat(columnNames); 722 } 723 else if ($.isPlainObject(columnNames) && columnNames.fieldKey) { 724 fieldKeys.push(columnNames.fieldKey.toString()); 725 } 726 727 // support fieldKeys (e.g. ["ColumnA", "ColumnA/Sub1"]) 728 // A special case of fieldKey is "SUBJECT_PREFIX/", used by participant group facet 729 if (fieldKeys.length > 0) { 730 $.each(_getParameters(this), function(i, param) { 731 var p = param[0]; 732 if (p.indexOf(me.name + '.') === 0 && p.indexOf('~') > -1) { 733 $.each(fieldKeys, function(j, name) { 734 var postfix = name && name.length && name[name.length - 1] == '/' ? '' : '~'; 735 if (p.indexOf(me.name + '.' + name + postfix) > -1) { 736 filterPrefixes.push(p); 737 } 738 }); 739 } 740 }); 741 } 742 743 _setParameters(this, filterParams, [OFFSET_PREFIX].concat($.unique(filterPrefixes))); 744 }; 745 746 /** 747 * @private 748 * @param filter 749 * @param filterMatch 750 */ 751 LABKEY.DataRegion.prototype.replaceFilterMatch = function(filter, filterMatch) { 752 var skips = [], me = this; 753 754 $.each(_getParameters(this), function(i, param) { 755 if (param[0].indexOf(me.name + '.') == 0 && param[0].indexOf(filterMatch) > -1) { 756 skips.push(param[0]); 757 } 758 }); 759 760 _updateFilter(this, filter, skips); 761 }; 762 763 // 764 // Selection 765 // 766 767 /** 768 * @private 769 */ 770 LABKEY.DataRegion.prototype._initSelection = function() { 771 772 var me = this, 773 form = _getFormSelector(this); 774 775 if (form && form.length) { 776 // backwards compatibility -- some references use this directly 777 // if you're looking to use this internally to the region use _getFormSelector() instead 778 this.form = form[0]; 779 } 780 781 if (form && this.showRecordSelectors) { 782 _onSelectionChange(this); 783 } 784 785 // Bind Events 786 _getAllRowSelectors(this).on('click', function(evt) { 787 evt.stopPropagation(); 788 me.selectPage.call(me, this.checked); 789 }); 790 _getRowSelectors(this).on('click', function() { me.selectRow.call(me, this); }); 791 }; 792 793 /** 794 * Clear all selected items for the current DataRegion. 795 * 796 * @param config A configuration object with the following properties: 797 * @param {Function} config.success The function to be called upon success of the request. 798 * The callback will be passed the following parameters: 799 * <ul> 800 * <li><b>data:</b> an object with the property 'count' of 0 to indicate an empty selection. 801 * <li><b>response:</b> The XMLHttpResponse object</li> 802 * </ul> 803 * @param {Function} [config.failure] The function to call upon error of the request. 804 * The callback will be passed the following parameters: 805 * <ul> 806 * <li><b>errorInfo:</b> an object containing detailed error information (may be null)</li> 807 * <li><b>response:</b> The XMLHttpResponse object</li> 808 * </ul> 809 * @param {Object} [config.scope] An optional scoping object for the success and error callback functions (default to this). 810 * @param {string} [config.containerPath] An alternate container path. If not specified, the current container path will be used. 811 * 812 * @see LABKEY.DataRegion#selectPage 813 * @see LABKEY.DataRegion.clearSelected static method. 814 */ 815 LABKEY.DataRegion.prototype.clearSelected = function(config) { 816 config = config || {}; 817 config.selectionKey = this.selectionKey; 818 config.scope = config.scope || this; 819 820 this.selectedCount = 0; 821 _onSelectionChange(this); 822 823 if (config.selectionKey) { 824 LABKEY.DataRegion.clearSelected(config); 825 } 826 827 if (this.showRows == 'selected') { 828 _removeParameters(this, [SHOW_ROWS_PREFIX]); 829 } 830 else if (this.showRows == 'unselected') { 831 // keep "SHOW_ROWS_PREFIX=unselected" parameter 832 window.location.reload(true); 833 } 834 else { 835 _toggleAllRows(this, false); 836 this.removeMessage('selection'); 837 } 838 }; 839 840 /** 841 * Get selected items on the current page of the DataRegion. 842 * Note, if the region is paginated, selected items may exist on other pages. 843 * @see LABKEY.DataRegion#getSelected 844 */ 845 LABKEY.DataRegion.prototype.getChecked = function() { 846 var values = []; 847 _getRowSelectors(this).each(function() { 848 if (this.checked) { 849 values.push(this.value); 850 } 851 }); 852 return values; 853 }; 854 855 /** 856 * Get all selected items for this DataRegion. 857 * 858 * @param config A configuration object with the following properties: 859 * @param {Function} config.success The function to be called upon success of the request. 860 * The callback will be passed the following parameters: 861 * <ul> 862 * <li><b>data:</b> an object with the property 'selected' that is an array of the primary keys for the selected rows. 863 * <li><b>response:</b> The XMLHttpResponse object</li> 864 * </ul> 865 * @param {Function} [config.failure] The function to call upon error of the request. 866 * The callback will be passed the following parameters: 867 * <ul> 868 * <li><b>errorInfo:</b> an object containing detailed error information (may be null)</li> 869 * <li><b>response:</b> The XMLHttpResponse object</li> 870 * </ul> 871 * @param {Object} [config.scope] An optional scoping object for the success and error callback functions (default to this). 872 * @param {string} [config.containerPath] An alternate container path. If not specified, the current container path will be used. 873 * 874 * @see LABKEY.DataRegion.getSelected static method. 875 */ 876 LABKEY.DataRegion.prototype.getSelected = function(config) { 877 if (!this.selectionKey) 878 return; 879 880 config = config || {}; 881 config.selectionKey = this.selectionKey; 882 LABKEY.DataRegion.getSelected(config); 883 }; 884 885 /** 886 * Returns the number of selected rows on the current page of the DataRegion. Selected items may exist on other pages. 887 * @returns {Integer} the number of selected rows on the current page of the DataRegion. 888 * @see LABKEY.DataRegion#getSelected to get all selected rows. 889 */ 890 LABKEY.DataRegion.prototype.getSelectionCount = function() { 891 if (!$('#' + this.domId)) { 892 return 0; 893 } 894 895 var count = 0; 896 _getRowSelectors(this).each(function() { 897 if (this.checked === true) { 898 count++; 899 } 900 }); 901 902 return count; 903 }; 904 905 /** 906 * Returns true if any row is checked on the current page of the DataRegion. Selected items may exist on other pages. 907 * @returns {Boolean} true if any row is checked on the current page of the DataRegion. 908 * @see LABKEY.DataRegion#getSelected to get all selected rows. 909 */ 910 LABKEY.DataRegion.prototype.hasSelected = function() { 911 return this.getSelectionCount() > 0; 912 }; 913 914 /** 915 * Returns true if all rows are checked on the current page of the DataRegion and at least one row is present. 916 * @returns {Boolean} true if all rows are checked on the current page of the DataRegion and at least one row is present. 917 * @see LABKEY.DataRegion#getSelected to get all selected rows. 918 */ 919 LABKEY.DataRegion.prototype.isPageSelected = function() { 920 var checkboxes = _getRowSelectors(this); 921 var i=0; 922 923 for (; i < checkboxes.length; i++) { 924 if (!checkboxes[i].checked) { 925 return false; 926 } 927 } 928 return i > 0; 929 }; 930 931 LABKEY.DataRegion.prototype.selectAll = function(config) { 932 if (this.selectionKey) { 933 config = config || {}; 934 config.scope = config.scope || this; 935 936 // Either use the selectAllURL provided or create a query config 937 // object that can be used with the generic query/selectAll.api action. 938 if (this.selectAllURL) { 939 config.url = this.selectAllURL; 940 } 941 else { 942 config = LABKEY.Utils.apply(config, this.getQueryConfig()); 943 } 944 945 config = _chainSelectionCountCallback(this, config); 946 947 LABKEY.DataRegion.selectAll(config); 948 949 if (this.showRows === "selected") { 950 // keep "SHOW_ROWS_PREFIX=selected" parameter 951 window.location.reload(true); 952 } 953 else if (this.showRows === "unselected") { 954 _removeParameters(this, [SHOW_ROWS_PREFIX]); 955 } 956 else { 957 _toggleAllRows(this, true); 958 } 959 } 960 }; 961 962 /** 963 * @deprecated use clearSelected instead 964 * @function 965 * @see LABKEY.DataRegion#clearSelected 966 */ 967 LABKEY.DataRegion.prototype.selectNone = LABKEY.DataRegion.prototype.clearSelected; 968 969 /** 970 * Set the selection state for all checkboxes on the current page of the DataRegion. 971 * @param checked whether all of the rows on the current page should be selected or unselected 972 * @returns {Array} Array of ids that were selected or unselected. 973 * 974 * @see LABKEY.DataRegion#setSelected to set selected items on the current page of the DataRegion. 975 * @see LABKEY.DataRegion#clearSelected to clear all selected. 976 */ 977 LABKEY.DataRegion.prototype.selectPage = function(checked) { 978 var _check = (checked === true); 979 var ids = _toggleAllRows(this, _check); 980 var me = this; 981 982 if (ids.length > 0) { 983 _getAllRowSelectors(this).each(function() { this.checked = _check}); 984 this.setSelected({ 985 ids: ids, 986 checked: _check, 987 success: function(data) { 988 if (data && data.count > 0 && !this.complete) { 989 var count = data.count; 990 var msg; 991 if (me.totalRows) { 992 if (count == me.totalRows) { 993 msg = 'All <span class="labkey-strong">' + this.totalRows + '</span> rows selected.'; 994 } 995 else { 996 msg = 'Selected <span class="labkey-strong">' + count + '</span> of ' + this.totalRows + ' rows.'; 997 } 998 } 999 else { 1000 // totalRows isn't available when showing all rows. 1001 msg = 'Selected <span class="labkey-strong">' + count + '</span> rows.'; 1002 } 1003 _showSelectMessage(me, msg); 1004 } 1005 else { 1006 this.removeMessage('selection'); 1007 } 1008 } 1009 }); 1010 } 1011 1012 return ids; 1013 }; 1014 1015 /** 1016 * @ignore 1017 * @param el 1018 */ 1019 LABKEY.DataRegion.prototype.selectRow = function(el) { 1020 this.setSelected({ 1021 ids: [el.value], 1022 checked: el.checked 1023 }); 1024 1025 if (!el.checked) { 1026 this.removeMessage('selection'); 1027 } 1028 }; 1029 1030 /** 1031 * Add or remove items from the selection associated with the this DataRegion. 1032 * 1033 * @param config A configuration object with the following properties: 1034 * @param {Array} config.ids Array of primary key ids for each row to select/unselect. 1035 * @param {Boolean} config.checked If true, the ids will be selected, otherwise unselected. 1036 * @param {Function} [config.success] The function to be called upon success of the request. 1037 * The callback will be passed the following parameters: 1038 * <ul> 1039 * <li><b>data:</b> an object with the property 'count' to indicate the updated selection count. 1040 * <li><b>response:</b> The XMLHttpResponse object</li> 1041 * </ul> 1042 * @param {Function} [config.failure] The function to call upon error of the request. 1043 * The callback will be passed the following parameters: 1044 * <ul> 1045 * <li><b>errorInfo:</b> an object containing detailed error information (may be null)</li> 1046 * <li><b>response:</b> The XMLHttpResponse object</li> 1047 * </ul> 1048 * @param {Object} [config.scope] An optional scoping object for the success and error callback functions (default to this). 1049 * @param {string} [config.containerPath] An alternate container path. If not specified, the current container path will be used. 1050 * 1051 * @see LABKEY.DataRegion#getSelected to get the selected items for this DataRegion. 1052 * @see LABKEY.DataRegion#clearSelected to clear all selected items for this DataRegion. 1053 */ 1054 LABKEY.DataRegion.prototype.setSelected = function(config) { 1055 if (!config || !LABKEY.Utils.isArray(config.ids) || config.ids.length == 0) { 1056 return; 1057 } 1058 1059 var me = this; 1060 config = config || {}; 1061 config.selectionKey = this.selectionKey; 1062 config.scope = config.scope || me; 1063 1064 config = _chainSelectionCountCallback(this, config); 1065 1066 var failure = LABKEY.Utils.getOnFailure(config); 1067 if ($.isFunction(failure)) { 1068 config.failure = failure; 1069 } 1070 else { 1071 config.failure = function() { me.addMessage('Error sending selection.'); }; 1072 } 1073 1074 if (config.selectionKey) { 1075 LABKEY.DataRegion.setSelected(config); 1076 } 1077 else if ($.isFunction(config.success)) { 1078 // Don't send the selection change to the server if there is no selectionKey. 1079 // Call the success callback directly. 1080 config.success.call(config.scope, {count: this.getSelectionCount()}); 1081 } 1082 }; 1083 1084 // 1085 // Parameters 1086 // 1087 1088 /** 1089 * Removes all parameters from the DataRegion 1090 */ 1091 LABKEY.DataRegion.prototype.clearAllParameters = function() { 1092 //var event = $.Event('beforeclearallparameters'); 1093 // 1094 //$(this).trigger(event, this); 1095 // 1096 //if (event.isDefaultPrevented()) { 1097 // return; 1098 //} 1099 1100 if (this.async) { 1101 this.offset = 0; 1102 this.parameters = undefined; 1103 } 1104 1105 _removeParameters(this, [PARAM_PREFIX, OFFSET_PREFIX]); 1106 }; 1107 1108 /** 1109 * Returns the specified parameter from the URL. Note, this is not related specifically 1110 * to parameterized query values (e.g. setParameters()/getParameters()) 1111 * @param {String} paramName 1112 * @returns {*} 1113 */ 1114 LABKEY.DataRegion.prototype.getParameter = function(paramName) { 1115 var param = null; 1116 1117 $.each(_getParameters(this), function(i, pair) { 1118 if (pair.length > 0 && pair[0] == paramName) { 1119 param = pair.length > 1 ? pair[1] : ''; 1120 return false; 1121 } 1122 }); 1123 1124 return param; 1125 }; 1126 1127 /** 1128 * Get the parameterized query values for this query. These parameters 1129 * are named by the query itself. 1130 * @param {boolean} toLowercase If true, all parameter names will be converted to lowercase 1131 * returns params An Object of key/val pairs. 1132 */ 1133 LABKEY.DataRegion.prototype.getParameters = function(toLowercase) { 1134 1135 var params = this.parameters ? this.parameters : {}, 1136 re = new RegExp('^' + LABKEY.Utils.escapeRe(this.name) + PARAM_PREFIX.replace(/\./g, '\\.'), 'i'), 1137 name; 1138 1139 $.each(_getParameters(this), function(i, pair) { 1140 if (pair.length > 0 && pair[0].match(re)) { 1141 name = pair[0].replace(re, ''); 1142 if (toLowercase === true) { 1143 name = name.toLowerCase(); 1144 } 1145 1146 // URL parameters will override this.parameters values 1147 params[name] = pair[1]; 1148 } 1149 }); 1150 1151 return params; 1152 }; 1153 1154 /** 1155 * Set the parameterized query values for this query. These parameters 1156 * are named by the query itself. 1157 * @param {Mixed} params An Object or Array of Array key/val pairs. 1158 */ 1159 LABKEY.DataRegion.prototype.setParameters = function(params) { 1160 var event = $.Event('beforesetparameters'); 1161 1162 $(this).trigger(event); 1163 1164 if (event.isDefaultPrevented()) { 1165 return; 1166 } 1167 1168 var paramPrefix = this.name + PARAM_PREFIX, _params = []; 1169 var newParameters = this.parameters ? this.parameters : {}; 1170 1171 function applyParameters(pKey, pValue) { 1172 var key = pKey; 1173 if (pKey.indexOf(paramPrefix) !== 0) { 1174 key = paramPrefix + pKey; 1175 } 1176 newParameters[key.replace(paramPrefix, '')] = pValue; 1177 _params.push([key, pValue]); 1178 } 1179 1180 // convert Object into Array of Array pairs and prefix the parameter name if necessary. 1181 if (LABKEY.Utils.isObject(params)) { 1182 $.each(params, applyParameters); 1183 } 1184 else if (LABKEY.Utils.isArray(params)) { 1185 $.each(params, function(i, pair) { 1186 if (LABKEY.Utils.isArray(pair) && pair.length > 1) { 1187 applyParameters(pair[0], pair[1]); 1188 } 1189 }); 1190 } 1191 else { 1192 return; // invalid argument shape 1193 } 1194 1195 this.parameters = newParameters; 1196 1197 _setParameters(this, _params, [PARAM_PREFIX, OFFSET_PREFIX]); 1198 }; 1199 1200 /** 1201 * @ignore 1202 * @Deprecated 1203 */ 1204 LABKEY.DataRegion.prototype.getSearchString = function() { 1205 if (!LABKEY.Utils.isString(this.savedSearchString)) { 1206 this.savedSearchString = document.location.search.substring(1) /* strip the ? */ || ""; 1207 } 1208 return this.savedSearchString; 1209 }; 1210 1211 /** 1212 * @ignore 1213 * @Deprecated 1214 */ 1215 LABKEY.DataRegion.prototype.setSearchString = function(regionName, search) { 1216 this.savedSearchString = search || ""; 1217 // If the search string doesn't change and there is a hash on the url, the page won't reload. 1218 // Remove the hash by setting the full path plus search string. 1219 window.location.assign(window.location.pathname + "?" + this.savedSearchString); 1220 }; 1221 1222 // 1223 // Messaging 1224 // 1225 1226 /** 1227 * @private 1228 */ 1229 LABKEY.DataRegion.prototype._initMessaging = function() { 1230 if (!this.msgbox) { 1231 this.msgbox = new MessageArea(this); 1232 this.msgbox.on('rendermsg', function(evt, msgArea, parts) { _onRenderMessageArea(this, parts); }, this); 1233 } 1234 else { 1235 this.msgbox.bindRegion(this); 1236 } 1237 1238 if (this.messages) { 1239 this.msgbox.setMessages(this.messages); 1240 this.msgbox.render(); 1241 } 1242 }; 1243 1244 /** 1245 * Show a message in the header of this DataRegion. 1246 * @param {String / Object} config the HTML source of the message to be shown or a config object with the following properties: 1247 * <ul> 1248 * <li><strong>html</strong>: {String} the HTML source of the message to be shown.</li> 1249 * <li><strong>part</strong>: {String} The part of the message area to render the message to.</li> 1250 * <li><strong>duration</strong>: {Integer} The amount of time (in milliseconds) the message will stay visible.</li> 1251 * <li><strong>hideButtonPanel</strong>: {Boolean} If true the button panel (customize view, export, etc.) will be hidden if visible.</li> 1252 * <li><strong>append</strong>: {Boolean} If true the msg is appended to any existing content for the given part.</li> 1253 * </ul> 1254 * @param part The part of the message area to render the message to. Used to scope messages so they can be added 1255 * and removed without clearing other messages. 1256 */ 1257 LABKEY.DataRegion.prototype.addMessage = function(config, part) { 1258 this.hidePanel(); 1259 1260 if (LABKEY.Utils.isString(config)) { 1261 this.msgbox.addMessage(config, part); 1262 } 1263 else if (LABKEY.Utils.isObject(config)) { 1264 this.msgbox.addMessage(config.html, config.part || part, config.append); 1265 1266 if (config.hideButtonPanel) { 1267 this.hideButtonPanel(); 1268 } 1269 1270 if (config.duration) { 1271 var dr = this; var timeout = config.duration; 1272 setTimeout(function() { 1273 dr.removeMessage(config.part || part); 1274 _getHeaderSelector(dr).trigger('resize'); 1275 }, timeout); 1276 } 1277 } 1278 }; 1279 1280 /** 1281 * Clear the message box contents. 1282 */ 1283 LABKEY.DataRegion.prototype.clearMessage = function() { 1284 if (this.msgbox) this.msgbox.clear(); 1285 }; 1286 1287 /** 1288 * @param part The part of the message area to render the message to. Used to scope messages so they can be added 1289 * and removed without clearing other messages. 1290 * @return {String} The message for 'part'. Could be undefined. 1291 */ 1292 LABKEY.DataRegion.prototype.getMessage = function(part) { 1293 if (this.msgbox) { return this.msgbox.getMessage(part); } // else undefined 1294 }; 1295 1296 /** 1297 * @param part The part of the message area to render the message to. Used to scope messages so they can be added 1298 * and removed without clearing other messages. 1299 * @return {Boolean} true iff there is a message area for this region and it has the message keyed by 'part'. 1300 */ 1301 LABKEY.DataRegion.prototype.hasMessage = function(part) { 1302 return this.msgbox && this.msgbox.hasMessage(part); 1303 }; 1304 1305 /** 1306 * If a message is currently showing, hide it and clear out its contents 1307 * @param keepContent If true don't remove the message area content 1308 */ 1309 LABKEY.DataRegion.prototype.hideMessage = function(keepContent) { 1310 if (this.msgbox) { 1311 this.msgbox.hide(); 1312 1313 if (!keepContent) 1314 this.removeAllMessages(); 1315 } 1316 }; 1317 1318 /** 1319 * @ignore 1320 * @private 1321 * Toggle expand/collapse state for the message area content. 1322 */ 1323 LABKEY.DataRegion.prototype.toggleMessageArea = function() { 1324 if (this.msgbox && this.msgbox.isVisible()) { 1325 1326 if (this.msgbox.getToggleEl().hasClass('fa-minus')) 1327 this.msgbox.collapse(); 1328 else 1329 this.msgbox.expand(); 1330 } 1331 }; 1332 1333 /** 1334 * Returns true if a message is currently being shown for this DataRegion. Messages are shown as a header. 1335 * @return {Boolean} true if a message is showing. 1336 */ 1337 LABKEY.DataRegion.prototype.isMessageShowing = function() { 1338 return this.msgbox && this.msgbox.isVisible(); 1339 }; 1340 1341 /** 1342 * Removes all messages from this Data Region. 1343 */ 1344 LABKEY.DataRegion.prototype.removeAllMessages = function() { 1345 if (this.msgbox) { this.msgbox.removeAll(); } 1346 }; 1347 1348 /** 1349 * If a message is currently showing, remove the specified part 1350 */ 1351 LABKEY.DataRegion.prototype.removeMessage = function(part) { 1352 if (this.msgbox) { this.msgbox.removeMessage(part); } 1353 }; 1354 1355 /** 1356 * Show a message in the header of this DataRegion with a loading indicator. 1357 * @param html the HTML source of the message to be shown 1358 */ 1359 LABKEY.DataRegion.prototype.showLoadingMessage = function(html) { 1360 html = html || "Loading..."; 1361 this.addMessage('<div><span class="loading-indicator"> </span><em>' + html + '</em></div>', 'drloading'); 1362 }; 1363 1364 LABKEY.DataRegion.prototype.hideLoadingMessage = function() { 1365 this.removeMessage('drloading'); 1366 }; 1367 1368 /** 1369 * Show a success message in the header of this DataRegion. 1370 * @param html the HTML source of the message to be shown 1371 */ 1372 LABKEY.DataRegion.prototype.showSuccessMessage = function(html) { 1373 html = html || "Completed successfully."; 1374 this.addMessage('<div class="labkey-message">' + html + '</div>'); 1375 }; 1376 1377 /** 1378 * Show an error message in the header of this DataRegion. 1379 * @param html the HTML source of the message to be shown 1380 */ 1381 LABKEY.DataRegion.prototype.showErrorMessage = function(html) { 1382 html = html || "An error occurred."; 1383 this.addMessage('<div class="labkey-error">' + html + '</div>'); 1384 }; 1385 1386 /** 1387 * Show a message in the header of this DataRegion. 1388 * @param msg the HTML source of the message to be shown 1389 * @deprecated use addMessage(msg, part) instead. 1390 */ 1391 LABKEY.DataRegion.prototype.showMessage = function(msg) { 1392 if (this.msgbox) { 1393 this.msgbox.addMessage(msg); 1394 } 1395 }; 1396 1397 LABKEY.DataRegion.prototype.showMessageArea = function() { 1398 if (this.msgbox && this.msgbox.hasContent()) { 1399 this.msgbox.show(); 1400 } 1401 }; 1402 1403 // 1404 // Sorting 1405 // 1406 1407 /** 1408 * Replaces the sort on the given column, if present, or sets a brand new sort 1409 * @param {string or LABKEY.FieldKey} fieldKey name of the column to be sorted 1410 * @param {string} [sortDir=+] Set to '+' for ascending or '-' for descending 1411 */ 1412 LABKEY.DataRegion.prototype.changeSort = function(fieldKey, sortDir) { 1413 if (!fieldKey) 1414 return; 1415 1416 fieldKey = _resolveFieldKey(this, fieldKey); 1417 1418 var columnName = fieldKey.toString(); 1419 1420 var event = $.Event("beforesortchange"); 1421 1422 $(this).trigger(event, [this, columnName, sortDir]); 1423 1424 if (event.isDefaultPrevented()) { 1425 return; 1426 } 1427 1428 this._userSort = _alterSortString(this, this._userSort, fieldKey, sortDir); 1429 _setParameter(this, SORT_PREFIX, this._userSort, [SORT_PREFIX, OFFSET_PREFIX]); 1430 }; 1431 1432 /** 1433 * Removes the sort on a specified column 1434 * @param {string or LABKEY.FieldKey} fieldKey name of the column 1435 */ 1436 LABKEY.DataRegion.prototype.clearSort = function(fieldKey) { 1437 if (!fieldKey) 1438 return; 1439 1440 fieldKey = _resolveFieldKey(this, fieldKey); 1441 1442 var columnName = fieldKey.toString(); 1443 1444 var event = $.Event("beforeclearsort"); 1445 1446 $(this).trigger(event, [this, columnName]); 1447 1448 if (event.isDefaultPrevented()) { 1449 return; 1450 } 1451 1452 this._userSort = _alterSortString(this, this._userSort, fieldKey); 1453 if (this._userSort.length > 0) { 1454 _setParameter(this, SORT_PREFIX, this._userSort, [SORT_PREFIX, OFFSET_PREFIX]); 1455 } 1456 else { 1457 _removeParameters(this, [SORT_PREFIX, OFFSET_PREFIX]); 1458 } 1459 }; 1460 1461 /** 1462 * Returns the user sort from the URL. The sort is represented as an Array of objects of the form: 1463 * <ul> 1464 * <li><b>fieldKey</b>: {String} The field key of the sort. 1465 * <li><b>dir</b>: {String} The sort direction, either "+" or "-". 1466 * </ul> 1467 * @returns {Object} Object representing the user sort. 1468 */ 1469 LABKEY.DataRegion.prototype.getUserSort = function() { 1470 return _getUserSort(this); 1471 }; 1472 1473 // 1474 // Paging 1475 // 1476 1477 LABKEY.DataRegion.prototype._initPaging = function() { 1478 _getHeaderSelector(this).find('div.labkey-pagination').css('visibility', 'visible'); 1479 }; 1480 1481 /** 1482 * Forces the grid to show all rows, without any paging 1483 */ 1484 LABKEY.DataRegion.prototype.showAllRows = function() { 1485 _showRows(this, 'all'); 1486 }; 1487 1488 /** 1489 * @deprecated use showAllRows instead 1490 * @function 1491 * @see LABKEY.DataRegion#showAllRows 1492 */ 1493 LABKEY.DataRegion.prototype.showAll = LABKEY.DataRegion.prototype.showAllRows; 1494 1495 /** 1496 * Forces the grid to show only rows that have been selected 1497 */ 1498 LABKEY.DataRegion.prototype.showSelectedRows = function() { 1499 _showRows(this, 'selected'); 1500 }; 1501 /** 1502 * @deprecated use showSelectedRows instead 1503 * @function 1504 * @see LABKEY.DataRegion#showSelectedRows 1505 */ 1506 LABKEY.DataRegion.prototype.showSelected = LABKEY.DataRegion.prototype.showSelectedRows; 1507 1508 /** 1509 * Forces the grid to show only rows that have not been selected 1510 */ 1511 LABKEY.DataRegion.prototype.showUnselectedRows = function() { 1512 _showRows(this, 'unselected'); 1513 }; 1514 /** 1515 * @deprecated use showUnselectedRows instead 1516 * @function 1517 * @see LABKEY.DataRegion#showUnselectedRows 1518 */ 1519 LABKEY.DataRegion.prototype.showUnselected = LABKEY.DataRegion.prototype.showUnselectedRows; 1520 1521 /** 1522 * Forces the grid to do paging based on the current maximum number of rows 1523 */ 1524 LABKEY.DataRegion.prototype.showPaged = function() { 1525 if (_beforeRowsChange(this, null)) { // lol, what? Handing in null is so lame 1526 _removeParameters(this, [SHOW_ROWS_PREFIX]); 1527 } 1528 }; 1529 1530 /** 1531 * Displays the first page of the grid 1532 */ 1533 LABKEY.DataRegion.prototype.showFirstPage = function() { 1534 this.setPageOffset(0); 1535 }; 1536 /** 1537 * @deprecated use showFirstPage instead 1538 * @function 1539 * @see LABKEY.DataRegion#showFirstPage 1540 */ 1541 LABKEY.DataRegion.prototype.pageFirst = LABKEY.DataRegion.prototype.showFirstPage; 1542 1543 /** 1544 * Changes the current row offset for paged content 1545 * @param rowOffset row index that should be at the top of the grid 1546 */ 1547 LABKEY.DataRegion.prototype.setPageOffset = function(rowOffset) { 1548 var event = $.Event('beforeoffsetchange'); 1549 1550 $(this).trigger(event, [this, rowOffset]); 1551 1552 if (event.isDefaultPrevented()) { 1553 return; 1554 } 1555 1556 // clear sibling parameters 1557 this.showRows = undefined; 1558 1559 if (rowOffset != undefined) { 1560 _setParameter(this, OFFSET_PREFIX, rowOffset, [OFFSET_PREFIX, SHOW_ROWS_PREFIX]); 1561 } 1562 else { 1563 _removeParameters(this, [OFFSET_PREFIX, SHOW_ROWS_PREFIX]); 1564 } 1565 }; 1566 /** 1567 * @deprecated use setPageOffset instead 1568 * @function 1569 * @see LABKEY.DataRegion#setPageOffset 1570 */ 1571 LABKEY.DataRegion.prototype.setOffset = LABKEY.DataRegion.prototype.setPageOffset; 1572 1573 /** 1574 * Changes the maximum number of rows that the grid will display at one time 1575 * @param newmax the maximum number of rows to be shown 1576 */ 1577 LABKEY.DataRegion.prototype.setMaxRows = function(newmax) { 1578 var event = $.Event('beforemaxrowschange'); // Can't this just be a variant of _beforeRowsChange with an extra param? 1579 $(this).trigger(event, [this, newmax]); 1580 if (event.isDefaultPrevented()) { 1581 return; 1582 } 1583 1584 // clear sibling parameters 1585 this.showRows = undefined; 1586 this.offset = 0; 1587 1588 _setParameter(this, MAX_ROWS_PREFIX, newmax, [OFFSET_PREFIX, MAX_ROWS_PREFIX, SHOW_ROWS_PREFIX]); 1589 }; 1590 1591 // 1592 // Customize View 1593 // 1594 LABKEY.DataRegion.prototype._initCustomViews = function() { 1595 if (this.view && this.view.session && this.getMessage('customizeview') == undefined) { 1596 var msg; 1597 if (this.view.savable) { 1598 msg = (this.viewName ? "The current grid view '<em>" + LABKEY.Utils.encodeHtml(this.viewName) + "</em>'" : "The current <em><default></em> grid view") + " is unsaved."; 1599 msg += " "; 1600 msg += "<span class='labkey-button unsavedview-revert'>Revert</span>"; 1601 msg += " "; 1602 msg += "<span class='labkey-button unsavedview-edit'>Edit</span>"; 1603 msg += " "; 1604 msg += "<span class='labkey-button unsavedview-save'>Save</span>"; 1605 } 1606 else { 1607 msg = ("The current grid view has been customized."); 1608 msg += " "; 1609 msg += "<span class='labkey-button unsavedview-revert' title='Revert'>Revert</span>"; 1610 msg += " "; 1611 msg += "<span class='labkey-button unsavedview-edit'>Edit</span>"; 1612 } 1613 1614 // add the customize view message, the link handlers will get added after render in _onRenderMessageArea 1615 this.addMessage(msg, 'customizeview'); 1616 } 1617 }; 1618 1619 /** 1620 * Change the currently selected view to the named view 1621 * @param {Object} view An object which contains the following properties. 1622 * @param {String} [view.type] the type of view, either a 'view' or a 'report'. 1623 * @param {String} [view.viewName] If the type is 'view', then the name of the view. 1624 * @param {String} [view.reportId] If the type is 'report', then the report id. 1625 * @param {Object} urlParameters <b>NOTE: Experimental parameter; may change without warning.</b> A set of filter and sorts to apply as URL parameters when changing the view. 1626 */ 1627 LABKEY.DataRegion.prototype.changeView = function(view, urlParameters) { 1628 var event = $.Event('beforechangeview'); 1629 $(this).trigger(event, [this, view, urlParameters]); 1630 if (event.isDefaultPrevented()) { 1631 return; 1632 } 1633 1634 var paramValPairs = [], 1635 newSort = [], 1636 skipPrefixes = [OFFSET_PREFIX, SHOW_ROWS_PREFIX, VIEWNAME_PREFIX, REPORTID_PREFIX]; 1637 1638 // clear sibling parameters 1639 this.viewName = undefined; 1640 this.reportId = undefined; 1641 1642 if (view) { 1643 if (LABKEY.Utils.isString(view)) { 1644 paramValPairs.push([VIEWNAME_PREFIX, view]); 1645 this.viewName = view; 1646 } 1647 else if (view.type == 'report') { 1648 paramValPairs.push([REPORTID_PREFIX, view.reportId]); 1649 this.reportId = view.reportId; 1650 } 1651 else if (view.type == 'view' && view.viewName) { 1652 paramValPairs.push([VIEWNAME_PREFIX, view.viewName]); 1653 this.viewName = view.viewName; 1654 } 1655 } 1656 1657 if (urlParameters) { 1658 $.each(urlParameters.filter, function(i, filter) { 1659 paramValPairs.push(['.' + filter.fieldKey + '~' + filter.op, filter.value]); 1660 }); 1661 1662 if (urlParameters.sort && urlParameters.sort.length > 0) { 1663 $.each(urlParameters.sort, function(i, sort) { 1664 newSort.push((sort.dir == "+" ? "" : sort.dir) + sort.fieldKey); 1665 }); 1666 paramValPairs.push([SORT_PREFIX, newSort.join(',')]); 1667 } 1668 1669 if (urlParameters.containerFilter) { 1670 paramValPairs.push([CONTAINER_FILTER_NAME, urlParameters.containerFilter]); 1671 } 1672 1673 // removes all filter, sort, and container filter parameters 1674 skipPrefixes = skipPrefixes.concat([ 1675 ALL_FILTERS_SKIP_PREFIX, SORT_PREFIX, COLUMNS_PREFIX, CONTAINER_FILTER_NAME 1676 ]); 1677 } 1678 1679 // removes all filter, sort, and container filter parameters 1680 _setParameters(this, paramValPairs, skipPrefixes); 1681 }; 1682 1683 LABKEY.DataRegion.prototype.getQueryDetails = function(success, failure, scope) { 1684 1685 var userFilter = [], 1686 userSort = this.getUserSort(), 1687 userColumns = this.getParameter(this.name + COLUMNS_PREFIX), 1688 fields = [], 1689 viewName = (this.view && this.view.name) || this.viewName || ''; 1690 1691 $.each(this.getUserFilterArray(), function(i, filter) { 1692 userFilter.push({ 1693 fieldKey: filter.getColumnName(), 1694 op: filter.getFilterType().getURLSuffix(), 1695 value: filter.getValue() 1696 }); 1697 }); 1698 1699 $.each(userFilter, function(i, filter) { 1700 fields.push(filter.fieldKey); 1701 }); 1702 1703 $.each(userSort, function(i, sort) { 1704 fields.push(sort.fieldKey); 1705 }); 1706 1707 LABKEY.Query.getQueryDetails({ 1708 containerPath: this.containerPath, 1709 schemaName: this.schemaName, 1710 queryName: this.queryName, 1711 viewName: viewName, 1712 fields: fields, 1713 initializeMissingView: true, 1714 success: function(queryDetails) { 1715 success.call(scope || this, queryDetails, viewName, userColumns, userFilter, userSort); 1716 }, 1717 failure: failure, 1718 scope: scope 1719 }); 1720 }; 1721 1722 /** 1723 * Hides the customize view interface if it is visible. 1724 */ 1725 LABKEY.DataRegion.prototype.hideCustomizeView = function() { 1726 if (this.activePanelId === CUSTOM_VIEW_PANELID) { 1727 this.hidePanel(); 1728 this.showMessageArea(); 1729 } 1730 }; 1731 1732 /** 1733 * Show the customize view interface. 1734 * @param activeTab {[String]} Optional. One of "ColumnsTab", "FilterTab", or "SortTab". If no value is specified (or undefined), the ColumnsTab will be shown. 1735 */ 1736 LABKEY.DataRegion.prototype.showCustomizeView = function(activeTab) { 1737 var region = this; 1738 1739 var panelConfig = this.getPanelConfiguration(CUSTOM_VIEW_PANELID); 1740 1741 if (!panelConfig) { 1742 1743 // whistle while we wait 1744 var timerId = setTimeout(function() { 1745 timerId = 0; 1746 region.showLoadingMessage("Opening custom view designer..."); 1747 }, 500); 1748 1749 LABKEY.DataRegion.loadViewDesigner(function() { 1750 1751 var success = function(queryDetails, viewName, userColumns, userFilter, userSort) { 1752 timerId > 0 ? clearTimeout(timerId) : this.hideLoadingMessage(); 1753 1754 // If there was an error parsing the query, we won't be able to render the customize view panel. 1755 if (queryDetails.exception) { 1756 var viewSourceUrl = LABKEY.ActionURL.buildURL('query', 'viewQuerySource.view', this.containerPath, { 1757 schemaName: this.schemaName, 1758 'query.queryName': this.queryName 1759 }); 1760 var msg = LABKEY.Utils.encodeHtml(queryDetails.exception) + 1761 " <a target=_blank class='labkey-button' href='" + viewSourceUrl + "'>View Source</a>"; 1762 1763 this.showErrorMessage(msg); 1764 return; 1765 } 1766 1767 var minWidth = Math.max(700, Math.min(1000, _getHeaderSelector(this).width())); // >= 700 && <= 1000 1768 1769 this.customizeView = Ext4.create('LABKEY.internal.ViewDesigner.Designer', { 1770 renderTo: Ext4.getBody().createChild({tag: 'div', customizeView: true, style: {display: 'none'}}), 1771 width: minWidth, 1772 activeTab: activeTab, 1773 dataRegion: this, 1774 containerPath : this.containerPath, 1775 schemaName: this.schemaName, 1776 queryName: this.queryName, 1777 viewName: viewName, 1778 query: queryDetails, 1779 userFilter: userFilter, 1780 userSort: userSort, 1781 userColumns: userColumns, 1782 userContainerFilter: this.getUserContainerFilter(), 1783 allowableContainerFilters: this.allowableContainerFilters 1784 }); 1785 1786 this.customizeView.on('viewsave', function(designer, savedViewsInfo, urlParameters) { 1787 _onViewSave.apply(this, [this, designer, savedViewsInfo, urlParameters]); 1788 }, this); 1789 1790 this.customizeView.on({ 1791 beforedeleteview: function(cv, revert) { 1792 _beforeViewDelete(region, revert); 1793 }, 1794 deleteview: function(cv, success, json) { 1795 _onViewDelete(region, success, json); 1796 } 1797 }); 1798 1799 var first = true; 1800 1801 // Called when customize view needs to be shown 1802 var showFn = function(id, panel, element, callback, scope) { 1803 if (first) { 1804 panel.hide(); 1805 panel.getEl().appendTo(Ext4.get(element[0])); 1806 first = false; 1807 } 1808 panel.doLayout(); 1809 $(panel.getEl().dom).slideDown(undefined, function() { 1810 panel.show(); 1811 callback.call(scope); 1812 }); 1813 }; 1814 1815 // Called when customize view needs to be hidden 1816 var hideFn = function(id, panel, element, callback, scope) { 1817 $(panel.getEl().dom).slideUp(undefined, function() { 1818 panel.hide(); 1819 callback.call(scope); 1820 }); 1821 }; 1822 1823 this.publishPanel(CUSTOM_VIEW_PANELID, this.customizeView, showFn, hideFn, this); 1824 this.showPanel(CUSTOM_VIEW_PANELID); 1825 }; 1826 var failure = function() { 1827 timerId > 0 ? clearTimeout(timerId) : this.hideLoadingMessage(); 1828 }; 1829 1830 this.getQueryDetails(success, failure, this); 1831 }, region); 1832 } 1833 else { 1834 if (activeTab) { 1835 panelConfig.panel.setActiveDesignerTab(activeTab); 1836 } 1837 this.showPanel(CUSTOM_VIEW_PANELID); 1838 } 1839 }; 1840 1841 /** 1842 * @ignore 1843 * @private 1844 * Shows/Hides customize view depending on if it is currently shown 1845 */ 1846 LABKEY.DataRegion.prototype.toggleShowCustomizeView = function() { 1847 if (this.activePanelId === CUSTOM_VIEW_PANELID) { 1848 this.hideCustomizeView(); 1849 } 1850 else { 1851 this.showCustomizeView(undefined); 1852 } 1853 }; 1854 1855 var _defaultShow = function(panelId, panel, ribbon, cb, cbScope) { 1856 $('#' + panelId).slideDown(undefined, function() { 1857 cb.call(cbScope); 1858 }); 1859 }; 1860 1861 var _defaultHide = function(panelId, panel, ribbon, cb, cbScope) { 1862 $('#' + panelId).slideUp(undefined, function() { 1863 cb.call(cbScope); 1864 }); 1865 }; 1866 1867 LABKEY.DataRegion.prototype.publishPanel = function(panelId, panel, showFn, hideFn, scope) { 1868 this.panelConfigurations[panelId] = { 1869 panel: panel, 1870 show: $.isFunction(showFn) ? showFn : _defaultShow, 1871 hide: $.isFunction(hideFn) ? hideFn : _defaultHide, 1872 scope: scope 1873 }; 1874 return this; 1875 }; 1876 1877 LABKEY.DataRegion.prototype.getPanelConfiguration = function(panelId) { 1878 return this.panelConfigurations[panelId]; 1879 }; 1880 1881 /** 1882 * @ignore 1883 * Hides any panel that is currently visible. Returns a callback once the panel is hidden. 1884 */ 1885 LABKEY.DataRegion.prototype.hidePanel = function(callback, scope) { 1886 if (this.activePanelId) { 1887 var config = this.getPanelConfiguration(this.activePanelId); 1888 if (config) { 1889 1890 // find the ribbon container 1891 var ribbon = _getHeaderSelector(this).find('.labkey-ribbon'); 1892 1893 config.hide.call(config.scope || this, this.activePanelId, config.panel, ribbon, function() { 1894 this.activePanelId = undefined; 1895 ribbon.hide(); 1896 if ($.isFunction(callback)) { 1897 callback.call(scope || this); 1898 } 1899 LABKEY.Utils.signalWebDriverTest("dataRegionPanelHide"); 1900 $(this).trigger($.Event('afterpanelhide'), [this]); 1901 }, this); 1902 } 1903 } 1904 else { 1905 if ($.isFunction(callback)) { 1906 callback.call(scope || this); 1907 } 1908 } 1909 }; 1910 1911 LABKEY.DataRegion.prototype.showPanel = function(panelId, callback, scope) { 1912 1913 var config = this.getPanelConfiguration(panelId); 1914 1915 if (!config) { 1916 console.error('Unable to find panel for id (' + panelId + '). Use publishPanel() to register a panel to be shown.'); 1917 return; 1918 } 1919 1920 this.hideMessage(true); 1921 1922 this.hidePanel(function() { 1923 this.activePanelId = panelId; 1924 1925 // ensure the ribbon is visible 1926 var ribbon = _getHeaderSelector(this).find('.labkey-ribbon'); 1927 ribbon.show(); 1928 1929 config.show.call(config.scope || this, this.activePanelId, config.panel, ribbon, function() { 1930 if ($.isFunction(callback)) { 1931 callback.call(scope || this); 1932 } 1933 LABKEY.Utils.signalWebDriverTest("dataRegionPanelShow"); 1934 $(this).trigger($.Event('afterpanelshow'), [this]); 1935 }, this); 1936 }, this); 1937 }; 1938 1939 // 1940 // Misc 1941 // 1942 1943 /** 1944 * @private 1945 */ 1946 LABKEY.DataRegion.prototype._initHeaderLocking = function() { 1947 if (this._allowHeaderLock === true) { 1948 this.hLock = new HeaderLock(this); 1949 } 1950 }; 1951 1952 /** 1953 * @private 1954 */ 1955 LABKEY.DataRegion.prototype._initPanes = function() { 1956 var callbacks = _paneCache[this.name]; 1957 if (callbacks) { 1958 var me = this; 1959 $.each(callbacks, function(i, config) { 1960 config.cb.call(config.scope || me, me); 1961 }); 1962 delete _paneCache[this.name]; 1963 } 1964 }; 1965 1966 // These study specific functions/constants should be moved out of Data Region 1967 // and into their own dependency. 1968 1969 var COHORT_LABEL = '/Cohort/Label'; 1970 var ADV_COHORT_LABEL = '/InitialCohort/Label'; 1971 var COHORT_ENROLLED = '/Cohort/Enrolled'; 1972 var ADV_COHORT_ENROLLED = '/InitialCohort/Enrolled'; 1973 1974 /** 1975 * DO NOT CALL DIRECTLY. This method is private and only available for removing cohort/group filters 1976 * for this Data Region. 1977 * @param subjectColumn 1978 * @param groupNames 1979 * @private 1980 */ 1981 LABKEY.DataRegion.prototype._removeCohortGroupFilters = function(subjectColumn, groupNames) { 1982 var params = _getParameters(this); 1983 var skips = [], i, p, k; 1984 1985 var keys = [ 1986 subjectColumn + COHORT_LABEL, 1987 subjectColumn + ADV_COHORT_LABEL, 1988 subjectColumn + COHORT_ENROLLED, 1989 subjectColumn + ADV_COHORT_ENROLLED 1990 ]; 1991 1992 if ($.isArray(groupNames)) { 1993 for (k=0; k < groupNames.length; k++) { 1994 keys.push(subjectColumn + '/' + groupNames[k]); 1995 } 1996 } 1997 1998 for (i = 0; i < params.length; i++) { 1999 p = params[i][0]; 2000 if (p.indexOf(this.name + '.') == 0) { 2001 for (k=0; k < keys.length; k++) { 2002 if (p.indexOf(keys[k] + '~') > -1) { 2003 skips.push(p); 2004 k = keys.length; // break loop 2005 } 2006 } 2007 } 2008 } 2009 2010 _updateFilter(this, undefined, skips); 2011 }; 2012 2013 /** 2014 * DO NOT CALL DIRECTLY. This method is private and only available for replacing advanced cohort filters 2015 * for this Data Region. Remove if advanced cohorts are removed. 2016 * @param filter 2017 * @private 2018 */ 2019 LABKEY.DataRegion.prototype._replaceAdvCohortFilter = function(filter) { 2020 var params = _getParameters(this); 2021 var skips = [], i, p; 2022 2023 for (i = 0; i < params.length; i++) { 2024 p = params[i][0]; 2025 if (p.indexOf(this.name + '.') == 0) { 2026 if (p.indexOf(COHORT_LABEL) > -1 || p.indexOf(ADV_COHORT_LABEL) > -1 || p.indexOf(COHORT_ENROLLED) > -1 || p.indexOf(ADV_COHORT_ENROLLED)) { 2027 skips.push(p); 2028 } 2029 } 2030 } 2031 2032 _updateFilter(this, filter, skips); 2033 }; 2034 2035 /** 2036 * Looks for a column based on fieldKey, name, or caption (in that order) 2037 * @param columnIdentifier 2038 * @returns {*} 2039 */ 2040 LABKEY.DataRegion.prototype.getColumn = function(columnIdentifier) { 2041 2042 var column = null, // backwards compat 2043 isString = LABKEY.Utils.isString, 2044 cols = this.columns; 2045 2046 if (isString(columnIdentifier) && $.isArray(cols)) { 2047 $.each(['fieldKey', 'name', 'caption'], function(i, key) { 2048 $.each(cols, function(c, col) { 2049 if (isString(col[key]) && col[key] == columnIdentifier) { 2050 column = col; 2051 return false; 2052 } 2053 }); 2054 if (column) { 2055 return false; 2056 } 2057 }); 2058 } 2059 2060 return column; 2061 }; 2062 2063 /** 2064 * Returns a query config object suitable for passing into LABKEY.Query.selectRows() or other LABKEY.Query APIs. 2065 * @returns {Object} Object representing the query configuration that generated this grid. 2066 */ 2067 LABKEY.DataRegion.prototype.getQueryConfig = function() { 2068 var config = { 2069 dataRegionName: this.name, 2070 dataRegionSelectionKey: this.selectionKey, 2071 schemaName: this.schemaName, 2072 viewName: this.viewName, 2073 sort: this.getParameter(this.name + SORT_PREFIX), 2074 // NOTE: The parameterized query values from QWP are included 2075 parameters: this.getParameters(false), 2076 containerFilter: this.containerFilter 2077 }; 2078 2079 if (this.queryName) { 2080 config.queryName = this.queryName; 2081 } 2082 else if (this.sql) { 2083 config.sql = this.sql; 2084 } 2085 2086 var filters = this.getUserFilterArray(); 2087 if (filters.length > 0) { 2088 config.filters = filters; 2089 } 2090 2091 return config; 2092 }; 2093 2094 /** 2095 * Hide the ribbon panel. If visible the ribbon panel will be hidden. 2096 */ 2097 LABKEY.DataRegion.prototype.hideButtonPanel = function() { 2098 this.hidePanel(); 2099 this.showMessageArea(); 2100 }; 2101 2102 /** 2103 * Allows for asynchronous rendering of the Data Region. This region must be in "async" mode for 2104 * this to do anything. 2105 * @function 2106 * @param {String} [renderTo] - The element ID where to render the data region. If not given it will default to 2107 * the current renderTo target is. 2108 */ 2109 LABKEY.DataRegion.prototype.render = function(renderTo) { 2110 if (!this.RENDER_LOCK && this.async) { 2111 _convertRenderTo(this, renderTo); 2112 this.refresh(); 2113 } 2114 }; 2115 2116 /** 2117 * Show a ribbon panel. 2118 */ 2119 LABKEY.DataRegion.prototype.showButtonPanel = function(panelButton) { 2120 2121 var ribbon = _getHeaderSelector(this).find('.labkey-ribbon'), 2122 panelId = $(panelButton).attr('panel-toggle'), 2123 panelSel; 2124 2125 if (panelId) { 2126 2127 panelSel = $('#' + panelId); 2128 2129 // allow for toggling the state 2130 if (panelId === this.activePanelId) { 2131 this.hidePanel(); 2132 this.showMessageArea(); 2133 } 2134 else { 2135 // determine if the content needs to be moved to the ribbon 2136 if (ribbon.has(panelSel).length === 0) { 2137 panelSel.detach().appendTo(ribbon); 2138 } 2139 2140 // determine if this panel has been registered 2141 if (!this.getPanelConfiguration(panelId)) { 2142 this.publishPanel(panelId); 2143 } 2144 2145 this.showPanel(panelId); 2146 } 2147 } 2148 }; 2149 2150 LABKEY.DataRegion.prototype.loadFaceting = function(cb, scope) { 2151 2152 var region = this; 2153 2154 var onLoad = function() { 2155 region.facetLoaded = true; 2156 if ($.isFunction(cb)) { 2157 cb.call(scope || this); 2158 } 2159 }; 2160 2161 LABKEY.requiresExt4ClientAPI(function() { 2162 if (LABKEY.devMode) { 2163 // should match study/ParticipantFilter.lib.xml 2164 LABKEY.requiresScript([ 2165 '/study/ReportFilterPanel.js', 2166 '/study/ParticipantFilterPanel.js' 2167 ], function() { 2168 LABKEY.requiresScript('/dataregion/panel/Facet.js', onLoad); 2169 }); 2170 } 2171 else { 2172 LABKEY.requiresScript('/study/ParticipantFilter.min.js', function() { 2173 LABKEY.requiresScript('/dataregion/panel/Facet.js', onLoad); 2174 }); 2175 } 2176 }, this); 2177 }; 2178 2179 LABKEY.DataRegion.prototype.showFaceting = function() { 2180 if (this.facetLoaded) { 2181 if (!this.facet) { 2182 this.facet = LABKEY.dataregion.panel.Facet.display(this); 2183 } 2184 this.facet.toggleCollapse(); 2185 } 2186 else { 2187 this.loadFaceting(this.showFaceting, this); 2188 } 2189 }; 2190 2191 LABKEY.DataRegion.prototype.on = function(evt, callback, scope) { 2192 // Prevent from handing back the jQuery event itself. 2193 $(this).bind(evt, function() { callback.apply(scope || this, $(arguments).slice(1)); }); 2194 }; 2195 2196 LABKEY.DataRegion.prototype._onButtonClick = function(buttonId) { 2197 var item = this.findButtonById(this.buttonBar.items, buttonId); 2198 if (item && $.isFunction(item.handler)) { 2199 try { 2200 return item.handler.call(item.scope || this, this); 2201 } 2202 catch(ignore) {} 2203 } 2204 return false; 2205 }; 2206 2207 LABKEY.DataRegion.prototype.findButtonById = function(items, id) { 2208 if (!items || !items.length || items.length <= 0) { 2209 return null; 2210 } 2211 2212 var ret; 2213 for (var i = 0; i < items.length; i++) { 2214 if (items[i].id == id) { 2215 return items[i]; 2216 } 2217 ret = this.findButtonById(items[i].items, id); 2218 if (null != ret) { 2219 return ret; 2220 } 2221 } 2222 2223 return null; 2224 }; 2225 2226 LABKEY.DataRegion.prototype.headerLock = function() { return this._allowHeaderLock === true; }; 2227 2228 LABKEY.DataRegion.prototype.disableHeaderLock = function() { 2229 if (this.headerLock() && this.hLock) { 2230 this.hLock.disable(); 2231 } 2232 }; 2233 2234 /** 2235 * Add or remove an aggregate for a given column in the DataRegion query view. 2236 * @param viewName 2237 * @param colFieldKey 2238 * @param aggType 2239 */ 2240 LABKEY.DataRegion.prototype.toggleAggregateForCustomView = function(viewName, colFieldKey, aggType) { 2241 this.getQueryDetails(function(queryDetails) 2242 { 2243 var view = _getViewFromQueryDetails(queryDetails, viewName); 2244 if (view != null) 2245 { 2246 if (_queryDetailsContainsColumn(queryDetails, colFieldKey)) 2247 { 2248 var colAggregateTypes = []; 2249 $.each(view.aggregates, function (index, existingAgg) { 2250 if (existingAgg.fieldKey == colFieldKey) 2251 colAggregateTypes.push(existingAgg.type); 2252 }); 2253 2254 var index = colAggregateTypes.indexOf(aggType); 2255 if (index == -1) 2256 colAggregateTypes.push(aggType); 2257 else 2258 colAggregateTypes.splice(index, 1); 2259 2260 view = _applyAggregatesToCustomView(view, colFieldKey, colAggregateTypes); 2261 this._updateSessionCustomView(view, true); 2262 } 2263 } 2264 }, null, this); 2265 }; 2266 2267 /** 2268 * Remove a column from the given DataRegion query view. 2269 * @param viewName 2270 * @param colFieldKey 2271 */ 2272 LABKEY.DataRegion.prototype.removeColumn = function(viewName, colFieldKey) { 2273 this.getQueryDetails(function(queryDetails) 2274 { 2275 var view = _getViewFromQueryDetails(queryDetails, viewName); 2276 if (view != null) 2277 { 2278 if (_queryDetailsContainsColumn(queryDetails, colFieldKey)) 2279 { 2280 var colFieldKeys = $.map(view.columns, function (c) { 2281 return c.fieldKey; 2282 }), 2283 fieldKeyIndex = colFieldKeys.indexOf(colFieldKey); 2284 2285 if (fieldKeyIndex > -1) 2286 { 2287 view.columns.splice(fieldKeyIndex, 1); 2288 this._updateSessionCustomView(view, true); 2289 } 2290 } 2291 } 2292 }, null, this); 2293 }; 2294 2295 /** 2296 * Add the enabled analytics provider to the custom view definition based on the column fieldKey and provider name. 2297 * In addition, disable the column menu item if the column is visible in the grid. 2298 * @param viewName 2299 * @param colFieldKey 2300 * @param providerName 2301 */ 2302 LABKEY.DataRegion.prototype.addAnalyticsProviderForCustomView = function(viewName, colFieldKey, providerName) { 2303 this.getQueryDetails(function(queryDetails) 2304 { 2305 var view = _getViewFromQueryDetails(queryDetails, viewName); 2306 if (view != null) 2307 { 2308 if (_queryDetailsContainsColumn(queryDetails, colFieldKey)) 2309 { 2310 var colProviderNames = []; 2311 $.each(view.analyticsProviders, function (index, existingProvider) { 2312 if (existingProvider.fieldKey == colFieldKey) 2313 colProviderNames.push(existingProvider.name); 2314 }); 2315 2316 if (colProviderNames.indexOf(providerName) == -1) 2317 { 2318 view.analyticsProviders.push({ 2319 fieldKey: colFieldKey, 2320 name: providerName 2321 }); 2322 2323 this._updateSessionCustomView(view, false); 2324 } 2325 2326 var elementId = this.name + ':' + colFieldKey + ':analytics-' + providerName; 2327 Ext4.each(Ext4.ComponentQuery.query('menuitem[elementId=' + elementId + ']'), function(menuItem) { 2328 menuItem.disable(); 2329 }); 2330 } 2331 } 2332 }, null, this); 2333 }; 2334 2335 /** 2336 * Remove an enabled analytics provider from the custom view definition based on the column fieldKey and provider name. 2337 * In addition, enable the column menu item if the column is visible in the grid. 2338 * @param viewName 2339 * @param colFieldKey 2340 * @param providerName 2341 */ 2342 LABKEY.DataRegion.prototype.removeAnalyticsProviderForCustomView = function(viewName, colFieldKey, providerName) { 2343 this.getQueryDetails(function(queryDetails) 2344 { 2345 var view = _getViewFromQueryDetails(queryDetails, viewName); 2346 if (view != null) 2347 { 2348 if (_queryDetailsContainsColumn(queryDetails, colFieldKey)) 2349 { 2350 var indexToRemove = null; 2351 $.each(view.analyticsProviders, function (index, existingProvider) { 2352 if (existingProvider.fieldKey == colFieldKey && existingProvider.name == providerName) { 2353 indexToRemove = index; 2354 return false; 2355 } 2356 }); 2357 2358 if (indexToRemove != null) { 2359 view.analyticsProviders.splice(indexToRemove, 1); 2360 this._updateSessionCustomView(view, false); 2361 } 2362 2363 var elementId = this.name + ':' + colFieldKey + ':analytics-' + providerName; 2364 Ext4.each(Ext4.ComponentQuery.query('menuitem[elementId=' + elementId + ']'), function(menuItem) { 2365 menuItem.enable(); 2366 }); 2367 } 2368 } 2369 }, null, this); 2370 }; 2371 2372 /** 2373 * @private 2374 */ 2375 LABKEY.DataRegion.prototype._openFilter = function(columnName) { 2376 if (this._dialogLoaded) { 2377 new LABKEY.FilterDialog({ 2378 dataRegionName: this.name, 2379 column: this.getColumn(columnName), 2380 cacheFacetResults: false // could have changed on Ajax 2381 }).show(); 2382 } 2383 else { 2384 LABKEY.requiresExt3ClientAPI(function() { 2385 this._dialogLoaded = true; 2386 new LABKEY.FilterDialog({ 2387 dataRegionName: this.name, 2388 column: this.getColumn(columnName), 2389 cacheFacetResults: false // could have changed on Ajax 2390 }).show(); 2391 }, this); 2392 } 2393 }; 2394 2395 LABKEY.DataRegion.prototype._updateSessionCustomView = function(customView, requiresRefresh) { 2396 var viewConfig = $.extend({}, customView, { 2397 shared: false, 2398 inherit: false, 2399 session: true 2400 }); 2401 2402 LABKEY.Query.saveQueryViews({ 2403 containerPath: this.containerFilter, 2404 schemaName: this.schemaName, 2405 queryName: this.queryName, 2406 views: [viewConfig], 2407 scope: this, 2408 success: function(info) { 2409 if (requiresRefresh) { 2410 this.refresh(); 2411 } 2412 else if (info.views.length == 1) { 2413 this.view = info.views[0]; 2414 this._initCustomViews(); 2415 } 2416 } 2417 }); 2418 }; 2419 2420 // 2421 // PRIVATE FUNCTIONS 2422 // 2423 var _applyOptionalParameters = function(region, params, optionalParams) { 2424 $.each(optionalParams, function(i, p) { 2425 if (LABKEY.Utils.isObject(p)) { 2426 if (region[p.name] !== undefined) { 2427 if (p.check && !p.check.call(region, region[p.name])) { 2428 return; 2429 } 2430 if (p.prefix) { 2431 params[region.name + '.' + p.name] = region[p.name]; 2432 } 2433 else { 2434 params[p.name] = region[p.name]; 2435 } 2436 } 2437 } 2438 else if (p && region[p] !== undefined) { 2439 params[p] = region[p]; 2440 } 2441 }); 2442 }; 2443 2444 var _alterSortString = function(region, current, fieldKey, direction /* optional */) { 2445 fieldKey = _resolveFieldKey(region, fieldKey); 2446 2447 var columnName = fieldKey.toString(), 2448 newSorts = []; 2449 2450 if (current != null) { 2451 var sorts = current.split(','); 2452 $.each(sorts, function(i, sort) { 2453 if (sort.length > 0 && (sort != columnName) && (sort != SORT_ASC + columnName) && (sort != SORT_DESC + columnName)) { 2454 newSorts.push(sort); 2455 } 2456 }); 2457 } 2458 2459 if (direction == SORT_ASC) { // Easier to read without the encoded + on the URL... 2460 direction = ''; 2461 } 2462 2463 if (LABKEY.Utils.isString(direction)) { 2464 newSorts = [direction + columnName].concat(newSorts); 2465 } 2466 2467 return newSorts.join(','); 2468 }; 2469 2470 var _beforeRowsChange = function(region, rowChangeEnum) { 2471 //var event = $.Event('beforeshowrowschange'); 2472 //$(region).trigger(event, [region, rowChangeEnum]); 2473 //if (event.isDefaultPrevented()) { 2474 // return false; 2475 //} 2476 return true; 2477 }; 2478 2479 var _buildQueryString = function(region, pairs) { 2480 if (!$.isArray(pairs)) { 2481 return ''; 2482 } 2483 2484 var queryParts = [], key, value; 2485 2486 $.each(pairs, function(i, pair) { 2487 key = pair[0]; 2488 value = pair.length > 1 ? pair[1] : undefined; 2489 2490 queryParts.push(encodeURIComponent(key)); 2491 if (LABKEY.Utils.isDefined(value)) { 2492 2493 if (LABKEY.Utils.isDate(value)) { 2494 value = $.format.date(value, 'yyyy-MM-dd'); 2495 if (LABKEY.Utils.endsWith(value, 'Z')) { 2496 value = value.substring(0, value.length - 1); 2497 } 2498 } 2499 queryParts.push('='); 2500 queryParts.push(encodeURIComponent(value)); 2501 } 2502 queryParts.push('&'); 2503 }); 2504 2505 if (queryParts.length > 0) { 2506 queryParts.pop(); 2507 } 2508 2509 return queryParts.join(""); 2510 }; 2511 2512 var _chainSelectionCountCallback = function(region, config) { 2513 2514 var success = LABKEY.Utils.getOnSuccess(config); 2515 2516 // On success, update the current selectedCount on this DataRegion and fire the 'selectchange' event 2517 config.success = function(data) { 2518 region.selectionModified = true; 2519 region.selectedCount = data.count; 2520 _onSelectionChange(region); 2521 2522 // Chain updateSelected with the user-provided success callback 2523 if ($.isFunction(success)) { 2524 success.call(config.scope, data); 2525 } 2526 }; 2527 2528 return config; 2529 }; 2530 2531 var _convertRenderTo = function(region, renderTo) { 2532 if (renderTo) { 2533 if (LABKEY.Utils.isString(renderTo)) { 2534 region.renderTo = renderTo; 2535 } 2536 else if (LABKEY.Utils.isString(renderTo.id)) { 2537 region.renderTo = renderTo.id; // support 'Ext' elements 2538 } 2539 else { 2540 throw 'Unsupported "renderTo"'; 2541 } 2542 } 2543 2544 return region; 2545 }; 2546 2547 var _deleteTimer; 2548 2549 var _beforeViewDelete = function(region, revert) { 2550 _deleteTimer = setTimeout(function() { 2551 _deleteTimer = 0; 2552 region.showLoadingMessage(revert ? 'Reverting view...' : 'Deleting view...'); 2553 }, 500); 2554 }; 2555 2556 var _onViewDelete = function(region, success, json) { 2557 if (_deleteTimer) { 2558 clearTimeout(_deleteTimer); 2559 } 2560 2561 if (success) { 2562 region.removeMessage.call(region, 'customizeview'); 2563 region.showSuccessMessage.call(region); 2564 2565 // change view to either a shadowed view or the default view 2566 var config = { type: 'view' }; 2567 if (json.viewName) { 2568 config.viewName = json.viewName; 2569 } 2570 region.changeView.call(region, config); 2571 } 2572 else { 2573 region.removeMessage.call(region, 'customizeview'); 2574 region.showErrorMessage.call(region, json.exception); 2575 } 2576 }; 2577 2578 // The view can be reverted without ViewDesigner present 2579 var _revertCustomView = function(region) { 2580 _beforeViewDelete(region, true); 2581 2582 var config = { 2583 schemaName: region.schemaName, 2584 queryName: region.queryName, 2585 revert: true, 2586 success: function(json) { 2587 _onViewDelete(region, true /* success */, json); 2588 }, 2589 failure: function(json) { 2590 _onViewDelete(region, false /* success */, json); 2591 } 2592 }; 2593 2594 if (region.viewName) { 2595 config.viewName = region.viewName; 2596 } 2597 2598 LABKEY.Query.deleteQueryView(config); 2599 }; 2600 2601 var _getViewFromQueryDetails = function(queryDetails, viewName) 2602 { 2603 var matchingView = null; 2604 2605 $.each(queryDetails.views, function(index, view) 2606 { 2607 if (view.name == viewName) 2608 { 2609 matchingView = view; 2610 return false; 2611 } 2612 }); 2613 2614 return matchingView; 2615 }; 2616 2617 var _queryDetailsContainsColumn = function(queryDetails, colFieldKey) 2618 { 2619 var keys = $.map(queryDetails.columns, function(c){ return c.fieldKey; }), 2620 exists = keys.indexOf(colFieldKey) > -1; 2621 2622 if (!exists) { 2623 console.warn('Unable to find column in query: ' + colFieldKey); 2624 } 2625 2626 return exists; 2627 }; 2628 2629 var _applyAggregatesToCustomView = function(customView, fieldKey, newAggregates) 2630 { 2631 // first, keep any existing custom view aggregates that don't match this fieldKey 2632 var aggregates = []; 2633 $.each(customView.aggregates, function(index, existingAgg) 2634 { 2635 if (existingAgg.fieldKey != fieldKey) 2636 aggregates.push(existingAgg); 2637 }); 2638 2639 // then add on the aggregates for the fieldKey selected 2640 $.each(newAggregates, function(index, newAggType) 2641 { 2642 aggregates.push({fieldKey: fieldKey, type: newAggType}); 2643 }); 2644 2645 customView.aggregates = aggregates; 2646 2647 return customView; 2648 }; 2649 2650 var _getAllRowSelectors = function(region) { 2651 return _getFormSelector(region).find('.labkey-selectors input[type="checkbox"][name=".toggle"]'); 2652 }; 2653 2654 var _getFormSelector = function(region) { 2655 var form = $('form#' + region.domId + '-form'); 2656 2657 // derived DataRegion's may not include the form id 2658 if (form.length == 0) { 2659 form = $('#' + region.domId).closest('form'); 2660 } 2661 2662 return form; 2663 }; 2664 2665 var _getRowSelectors = function(region) { 2666 return _getFormSelector(region).find('.labkey-selectors input[type="checkbox"][name=".select"]'); 2667 }; 2668 2669 var _getHeaderSelector = function(region) { 2670 return $('#' + region.domId + '-header'); 2671 }; 2672 2673 // Formerly, LABKEY.DataRegion.getParamValPairsFromString / LABKEY.DataRegion.getParamValPairs 2674 var _getParameters = function(region, skipPrefixSet /* optional */) { 2675 2676 var params = []; 2677 var qString = region.requestURL; 2678 2679 if (LABKEY.Utils.isString(qString) && qString.length > 0) { 2680 2681 var qmIdx = qString.indexOf('?'); 2682 if (qmIdx > -1) { 2683 qString = qString.substring(qmIdx + 1); 2684 } 2685 2686 if (qString.length > 1) { 2687 var pairs = qString.split('&'), p, key, 2688 LAST = '.lastFilter', lastIdx, skip = $.isArray(skipPrefixSet); 2689 2690 $.each(pairs, function(i, pair) { 2691 p = pair.split('=', 2); 2692 key = p[0] = decodeURIComponent(p[0]); 2693 lastIdx = key.indexOf(LAST); 2694 2695 if (lastIdx > -1 && lastIdx == (key.length - LAST.length)) { 2696 return; 2697 } 2698 else if (REQUIRE_NAME_PREFIX.hasOwnProperty(key)) { 2699 // 26686: Black list known parameters, should be prefixed by region name 2700 return; 2701 } 2702 2703 var stop = false; 2704 if (skip) { 2705 $.each(skipPrefixSet, function(j, skipPrefix) { 2706 if (LABKEY.Utils.isString(skipPrefix)) { 2707 2708 // Special prefix that should remove all filters, but no other parameters 2709 if (skipPrefix.indexOf(ALL_FILTERS_SKIP_PREFIX) == (skipPrefix.length - 2)) { 2710 if (key.indexOf('~') > 0) { 2711 stop = true; 2712 return false; 2713 } 2714 } 2715 else if (key.indexOf(skipPrefix) == 0) { 2716 // only skip filters, parameters, and sorts 2717 if (key == skipPrefix || 2718 key.indexOf('~') > 0 || 2719 key.indexOf(PARAM_PREFIX) > 0 || 2720 key == (skipPrefix + 'sort')) { 2721 stop = true; 2722 return false; 2723 } 2724 } 2725 } 2726 }); 2727 } 2728 2729 if (!stop) { 2730 if (p.length > 1) { 2731 p[1] = decodeURIComponent(p[1]); 2732 } 2733 params.push(p); 2734 } 2735 }); 2736 } 2737 } 2738 2739 return params; 2740 }; 2741 2742 /** 2743 * 2744 * @param region 2745 * @param {boolean} [asString=false] 2746 * @private 2747 */ 2748 var _getUserSort = function(region, asString) { 2749 var userSort = [], 2750 sortParam = region.getParameter(region.name + SORT_PREFIX); 2751 2752 if (asString) { 2753 userSort = sortParam || ''; 2754 } 2755 else { 2756 if (sortParam) { 2757 var fieldKey, dir; 2758 $.each(sortParam.split(','), function(i, sort) { 2759 fieldKey = sort; 2760 dir = SORT_ASC; 2761 if (sort.charAt(0) == SORT_DESC) { 2762 fieldKey = fieldKey.substring(1); 2763 dir = SORT_DESC; 2764 } 2765 else if (sort.charAt(0) == SORT_ASC) { 2766 fieldKey = fieldKey.substring(1); 2767 } 2768 userSort.push({fieldKey: fieldKey, dir: dir}); 2769 }); 2770 } 2771 } 2772 2773 return userSort; 2774 }; 2775 2776 var _buttonBind = function(region, cls, fn) { 2777 region.msgbox.find('.labkey-button' + cls).off('click').on('click', $.proxy(function() { 2778 fn.call(this); 2779 }, region)); 2780 }; 2781 2782 var _onRenderMessageArea = function(region, parts) { 2783 var msgArea = region.msgbox; 2784 if (msgArea) { 2785 if (region.showRecordSelectors && parts['selection']) { 2786 _buttonBind(region, '.select-all', region.selectAll); 2787 _buttonBind(region, '.select-none', region.clearSelected); 2788 _buttonBind(region, '.show-all', region.showAll); 2789 _buttonBind(region, '.show-selected', region.showSelected); 2790 _buttonBind(region, '.show-unselected', region.showUnselected); 2791 } 2792 else if (parts['customizeview']) { 2793 _buttonBind(region, '.unsavedview-revert', function() { _revertCustomView(this); }); 2794 _buttonBind(region, '.unsavedview-edit', function() { this.showCustomizeView(undefined); }); 2795 _buttonBind(region, '.unsavedview-save', function() { _saveSessionCustomView(this); }); 2796 } 2797 } 2798 }; 2799 2800 var _onSelectionChange = function(region) { 2801 $(region).trigger('selectchange', [region, region.selectedCount]); 2802 _updateRequiresSelectionButtons(region, region.selectedCount); 2803 LABKEY.Utils.signalWebDriverTest('dataRegionUpdate', region.selectedCount); 2804 }; 2805 2806 var _onViewSave = function(region, designer, savedViewsInfo, urlParameters) { 2807 if (savedViewsInfo && savedViewsInfo.views.length > 0) { 2808 region.hideCustomizeView.call(region); 2809 region.changeView.call(region, { 2810 type: 'view', 2811 viewName: savedViewsInfo.views[0].name 2812 }, urlParameters); 2813 } 2814 }; 2815 2816 var _removeParameters = function(region, skipPrefixes /* optional */) { 2817 return _setParameters(region, null, skipPrefixes); 2818 }; 2819 2820 var _resolveFieldKey = function(region, fieldKey) { 2821 var fk = fieldKey; 2822 if (!(fk instanceof LABKEY.FieldKey)) { 2823 fk = LABKEY.FieldKey.fromString('' + fk); 2824 } 2825 return fk; 2826 }; 2827 2828 var _saveSessionCustomView = function(region) { 2829 // Note: currently only will save session views. Future version could create a new view using url sort/filters. 2830 if (!(region.view && region.view.session)) { 2831 return; 2832 } 2833 2834 // Get the canEditSharedViews permission and candidate targetContainers. 2835 var viewName = (region.view && region.view.name) || region.viewName || ''; 2836 2837 LABKEY.Query.getQueryDetails({ 2838 schemaName: region.schemaName, 2839 queryName: region.queryName, 2840 viewName: viewName, 2841 initializeMissingView: false, 2842 success: function (json) { 2843 // Display an error if there was an issue error getting the query details 2844 if (json.exception) { 2845 var viewSourceUrl = LABKEY.ActionURL.buildURL('query', 'viewQuerySource.view', null, {schemaName: this.schemaName, "query.queryName": this.queryName}); 2846 var msg = LABKEY.Utils.encodeHtml(json.exception) + " <a target=_blank class='labkey-button' href='" + viewSourceUrl + "'>View Source</a>"; 2847 2848 this.showErrorMessage.call(this, msg); 2849 return; 2850 } 2851 2852 _saveSessionShowPrompt(this, json); 2853 }, 2854 scope: region 2855 }); 2856 }; 2857 2858 var _saveSessionShowPrompt = function(region, queryDetails) { 2859 var config = Ext4.applyIf({ 2860 allowableContainerFilters: region.allowableContainerFilters, 2861 targetContainers: queryDetails.targetContainers, 2862 canEditSharedViews: queryDetails.canEditSharedViews, 2863 canEdit: LABKEY.DataRegion.getCustomViewEditableErrors(config).length == 0, 2864 success: function (win, o) { 2865 var timerId = setTimeout(function() { 2866 timerId = 0; 2867 Ext4.Msg.progress("Saving...", "Saving custom view..."); 2868 }, 500); 2869 2870 var jsonData = { 2871 schemaName: region.schemaName, 2872 "query.queryName": region.queryName, 2873 "query.viewName": region.viewName, 2874 newName: o.name, 2875 inherit: o.inherit, 2876 shared: o.shared 2877 }; 2878 2879 if (o.inherit) { 2880 jsonData.containerPath = o.containerPath; 2881 } 2882 2883 LABKEY.Ajax.request({ 2884 url: LABKEY.ActionURL.buildURL('query', 'saveSessionView', region.containerPath), 2885 method: 'POST', 2886 jsonData: jsonData, 2887 callback: function() { 2888 if (timerId > 0) 2889 clearTimeout(timerId); 2890 win.close(); 2891 }, 2892 success: function() { 2893 region.showSuccessMessage.call(region); 2894 region.changeView.call(region, {type: 'view', viewName: o.name}); 2895 }, 2896 failure: function(json) { 2897 Ext4.Msg.alert('Error saving view', json.exception || json.statusText); 2898 }, 2899 scope: region 2900 }); 2901 }, 2902 scope: region 2903 }, region.view); 2904 2905 LABKEY.DataRegion.loadViewDesigner(function() { 2906 LABKEY.internal.ViewDesigner.Designer.saveCustomizeViewPrompt(config); 2907 }); 2908 }; 2909 2910 var _setParameter = function(region, param, value, skipPrefixes /* optional */) { 2911 _setParameters(region, [[param, value]], skipPrefixes); 2912 }; 2913 2914 var _setParameters = function(region, newParamValPairs, skipPrefixes /* optional */) { 2915 2916 // prepend region name 2917 // e.g. ['.hello', '.goodbye'] becomes ['aqwp19.hello', 'aqwp19.goodbye'] 2918 if ($.isArray(skipPrefixes)) { 2919 $.each(skipPrefixes, function(i, skip) { 2920 if (skip && skip.indexOf(region.name + '.') !== 0) { 2921 skipPrefixes[i] = region.name + skip; 2922 } 2923 }); 2924 } 2925 2926 var param, value, 2927 params = _getParameters(region, skipPrefixes); 2928 2929 if ($.isArray(newParamValPairs)) { 2930 $.each(newParamValPairs, function(i, newPair) { 2931 if (!$.isArray(newPair)) { 2932 throw new Error("DataRegion: _setParameters newParamValPairs improperly initialized. It is an array of arrays. You most likely passed in an array of strings."); 2933 } 2934 param = newPair[0]; 2935 value = newPair[1]; 2936 2937 // Allow value to be null/undefined to support no-value filter types (Is Blank, etc) 2938 if (LABKEY.Utils.isString(param) && param.length > 1) { 2939 if (param.indexOf(region.name) !== 0) { 2940 param = region.name + param; 2941 } 2942 2943 params.push([param, value]); 2944 } 2945 }); 2946 } 2947 2948 if (region.async) { 2949 _load(region, undefined, undefined, params); 2950 } 2951 else { 2952 region.setSearchString.call(region, region.name, _buildQueryString(region, params)); 2953 } 2954 }; 2955 2956 var _showRows = function(region, showRowsEnum) { 2957 if (_beforeRowsChange(region, showRowsEnum)) { 2958 2959 // clear sibling parameters, could we do this with events? 2960 this.maxRows = undefined; 2961 this.offset = 0; 2962 2963 _setParameter(region, SHOW_ROWS_PREFIX, showRowsEnum, [OFFSET_PREFIX, MAX_ROWS_PREFIX, SHOW_ROWS_PREFIX]); 2964 } 2965 }; 2966 2967 var _showSelectMessage = function(region, msg) { 2968 if (region.showRecordSelectors) { 2969 if (region.totalRows && region.totalRows != region.selectedCount) { 2970 msg += " <span class='labkey-button select-all'>Select All " + region.totalRows + " Rows</span>"; 2971 } 2972 2973 msg += " " + "<span class='labkey-button select-none'>Select None</span>"; 2974 var showOpts = []; 2975 if (region.showRows != "all") 2976 showOpts.push("<span class='labkey-button show-all'>Show All</span>"); 2977 if (region.showRows != "selected") 2978 showOpts.push("<span class='labkey-button show-selected'>Show Selected</span>"); 2979 if (region.showRows != "unselected") 2980 showOpts.push("<span class='labkey-button show-unselected'>Show Unselected</span>"); 2981 msg += " " + showOpts.join(" "); 2982 } 2983 2984 // add the record selector message, the link handlers will get added after render in _onRenderMessageArea 2985 region.addMessage.call(region, msg, 'selection'); 2986 }; 2987 2988 var _toggleAllRows = function(region, checked) { 2989 var ids = []; 2990 2991 _getRowSelectors(region).each(function() { 2992 if (!this.disabled) { 2993 this.checked = checked; 2994 ids.push(this.value); 2995 } 2996 }); 2997 2998 _getAllRowSelectors(region).each(function() { this.checked = (checked == true)}); 2999 return ids; 3000 }; 3001 3002 var _load = function(region, callback, scope, newParams) { 3003 3004 var params = _getAsyncParams(region, newParams ? newParams : _getParameters(region)); 3005 var jsonData = _getAsyncBody(region, params); 3006 3007 // TODO: This should be done in _getAsyncParams, but is not since _getAsyncBody relies on it. Refactor it. 3008 // ensure SQL is not on the URL -- we allow any property to be pulled through when creating parameters. 3009 if (params.sql) { 3010 delete params.sql; 3011 } 3012 3013 /** 3014 * The target jQuery element that will be either written to or replaced 3015 */ 3016 var target; 3017 3018 /** 3019 * Flag used to determine if we should replace target element (default) or write to the target contents 3020 * (used during QWP render for example) 3021 * @type {boolean} 3022 */ 3023 var useReplace = true; 3024 3025 /** 3026 * The string identifier for where the region will render. Mainly used to display useful messaging upon failure. 3027 * @type {string} 3028 */ 3029 var renderEl; 3030 3031 if (region.renderTo) { 3032 useReplace = false; 3033 renderEl = region.renderTo; 3034 target = $('#' + region.renderTo); 3035 } 3036 else if (!region.domId) { 3037 throw '"renderTo" must be specified either upon construction or when calling render()'; 3038 } 3039 else { 3040 renderEl = region.domId; 3041 target = $('#' + region.domId); 3042 3043 // attempt to find the correct node to render to... 3044 var form = _getFormSelector(region); 3045 if (form.length && form.parent('div').length) { 3046 target = form.parent('div'); 3047 } 3048 else { 3049 // next best render target 3050 throw 'unable to find a good target element. Perhaps this region is not using the standard renderer?' 3051 } 3052 } 3053 3054 LABKEY.Ajax.request({ 3055 timeout: (region.timeout == undefined) ? DEFAULT_TIMEOUT : region.timeout, 3056 url: LABKEY.ActionURL.buildURL('project', 'getWebPart.api', region.containerPath), 3057 method: 'POST', 3058 params: params, 3059 jsonData: jsonData, 3060 success: function(response) { 3061 3062 this.hidePanel(function() { 3063 if (target.length) { 3064 3065 this.destroy(); 3066 3067 LABKEY.Utils.loadAjaxContent(response, target, function() { 3068 3069 if ($.isFunction(callback)) { 3070 callback.call(scope); 3071 } 3072 3073 if ($.isFunction(this._success)) { 3074 this._success.call(this.scope || this, this, response); 3075 } 3076 3077 $(this).trigger('success', [this, response]); 3078 3079 this.RENDER_LOCK = true; 3080 $(this).trigger('render', this); 3081 this.RENDER_LOCK = false; 3082 }, this, useReplace); 3083 } 3084 else { 3085 // not finding element considered a failure 3086 if ($.isFunction(this._failure)) { 3087 this._failure.call(this.scope || this, response /* json */, response, undefined /* options */, target); 3088 } 3089 else if (!this.suppressRenderErrors) { 3090 LABKEY.Utils.alert('Rendering Error', 'The element "' + renderEl + '" does not exist in the document. You may need to specify "renderTo".'); 3091 } 3092 } 3093 }, this); 3094 }, 3095 failure: LABKEY.Utils.getCallbackWrapper(function(json, response, options) { 3096 3097 if (target.length) { 3098 if ($.isFunction(this._failure)) { 3099 this._failure.call(this.scope || this, json, response, options); 3100 } 3101 else if (this.errorType === 'html') { 3102 if (useReplace) { 3103 target.replaceWith('<div class="labkey-error">' + LABKEY.Utils.encodeHtml(json.exception) + '</div>'); 3104 } 3105 else { 3106 target.html('<div class="labkey-error">' + LABKEY.Utils.encodeHtml(json.exception) + '</div>'); 3107 } 3108 } 3109 } 3110 else if (!this.suppressRenderErrors) { 3111 LABKEY.Utils.alert('Rendering Error', 'The element "' + renderEl + '" does not exist in the document. You may need to specify "renderTo".'); 3112 } 3113 }, region, true), 3114 scope: region 3115 }); 3116 }; 3117 3118 var _getAsyncBody = function(region, params) { 3119 var json = {}; 3120 3121 if (params.sql) { 3122 json.sql = params.sql; 3123 } 3124 3125 _processButtonBar(region, json); 3126 3127 // 10505: add non-removable sorts and filters to json (not url params). 3128 if (region.sort || region.filters || region.aggregates) { 3129 json.filters = {}; 3130 3131 if (region.filters) { 3132 LABKEY.Filter.appendFilterParams(json.filters, region.filters, region.name); 3133 } 3134 3135 if (region.sort) { 3136 json.filters[region.dataRegionName + SORT_PREFIX] = region.sort; 3137 } 3138 3139 if (region.aggregates) { 3140 LABKEY.Filter.appendAggregateParams(json.filters, region.aggregates, region.name); 3141 } 3142 } 3143 3144 if (region.metadata) { 3145 json.metadata = region.metadata; 3146 } 3147 3148 return json; 3149 }; 3150 3151 var _processButtonBar = function(region, json) { 3152 3153 var bar = region.buttonBar; 3154 3155 if (bar && (bar.position || (bar.items && bar.items.length > 0))) { 3156 _processButtonBarItems(region, bar.items); 3157 3158 // only attach if valid 3159 json.buttonBar = bar; 3160 } 3161 }; 3162 3163 var _processButtonBarItems = function(region, items) { 3164 if ($.isArray(items) && items.length > 0) { 3165 for (var i = 0; i < items.length; i++) { 3166 var item = items[i]; 3167 3168 if (item && $.isFunction(item.handler)) { 3169 item.id = item.id || LABKEY.Utils.id(); 3170 // TODO: A better way? This exposed _onButtonClick isn't very awesome 3171 item.onClick = "return LABKEY.DataRegions['" + region.name + "']._onButtonClick('" + item.id + "');"; 3172 } 3173 3174 if (item.items) { 3175 _processButtonBarItems(region, item.items); 3176 } 3177 } 3178 } 3179 }; 3180 3181 var _isFilter = function(region, parameter) { 3182 return parameter && parameter.indexOf(region.name + '.') === 0 && parameter.indexOf('~') > 0; 3183 }; 3184 3185 var _getAsyncParams = function(region, newParams) { 3186 3187 var params = {}; 3188 var name = region.name; 3189 3190 // 3191 // Certain parameters are only included if the region is 'async'. These 3192 // were formerly a part of Query Web Part. 3193 // 3194 if (region.async) { 3195 params[name + '.async'] = true; 3196 3197 if (LABKEY.Utils.isString(region.frame)) { 3198 params['webpart.frame'] = region.frame; 3199 } 3200 3201 if (LABKEY.Utils.isString(region.bodyClass)) { 3202 params['webpart.bodyClass'] = region.bodyClass; 3203 } 3204 3205 if (LABKEY.Utils.isString(region.title)) { 3206 params['webpart.title'] = region.title; 3207 } 3208 3209 if (LABKEY.Utils.isString(region.titleHref)) { 3210 params['webpart.titleHref'] = region.titleHref; 3211 } 3212 3213 _applyOptionalParameters(region, params, [ 3214 'allowChooseQuery', 3215 'allowChooseView', 3216 'allowHeaderLock', 3217 'buttonBarPosition', 3218 'detailsURL', 3219 'deleteURL', 3220 'importURL', 3221 'insertURL', 3222 'linkTarget', 3223 'updateURL', 3224 'shadeAlternatingRows', 3225 'showBorders', 3226 'showDeleteButton', 3227 'showDetailsColumn', 3228 'showExportButtons', 3229 'showInsertNewButton', 3230 'showPagination', 3231 'showReports', 3232 'showSurroundingBorder', 3233 'showUpdateColumn', 3234 'showViewPanel', 3235 'timeout', 3236 {name: 'disableAnalytics', prefix: true}, 3237 {name: 'maxRows', prefix: true, check: function(v) { return v > 0; }}, 3238 {name: 'showRows', prefix: true}, 3239 {name: 'offset', prefix: true, check: function(v) { return v !== 0; }}, 3240 {name: 'reportId', prefix: true}, 3241 {name: 'viewName', prefix: true} 3242 ]); 3243 3244 // Sorts configured by the user when interacting with the grid. We need to pass these as URL parameters. 3245 if (LABKEY.Utils.isString(region._userSort) && region._userSort.length > 0) { 3246 params[name + SORT_PREFIX] = region._userSort; 3247 } 3248 3249 if (region.userFilters) { 3250 $.each(region.userFilters, function(filterExp, filterValue) { 3251 if (params[filterExp] == undefined) { 3252 params[filterExp] = []; 3253 } 3254 params[filterExp].push(filterValue); 3255 }); 3256 region.userFilters = {}; // they've been applied 3257 } 3258 3259 // TODO: Get rid of this and incorporate it with the normal containerFilter checks 3260 if (region.userContainerFilter) { 3261 params[name + CONTAINER_FILTER_NAME] = region.userContainerFilter; 3262 } 3263 3264 if (region.parameters) { 3265 var paramPrefix = name + PARAM_PREFIX; 3266 $.each(region.parameters, function(parameter, value) { 3267 var key = parameter; 3268 if (parameter.indexOf(paramPrefix) !== 0) { 3269 key = paramPrefix + parameter; 3270 } 3271 params[key] = value; 3272 }); 3273 } 3274 } 3275 3276 // 3277 // apply all parameters 3278 // 3279 3280 if (newParams) { 3281 $.each(newParams, function(i, pair) { 3282 // 3283 // Filters may repeat themselves #25337 3284 // 3285 if (_isFilter(region, pair[0])) { 3286 if (params[pair[0]] == undefined) { 3287 params[pair[0]] = []; 3288 } 3289 else if (!$.isArray(params[pair[0]])) { 3290 params[pair[0]] = [params[pair[0]]]; 3291 } 3292 params[pair[0]].push(pair[1]); 3293 } 3294 else { 3295 params[pair[0]] = pair[1]; 3296 } 3297 }); 3298 } 3299 3300 // 3301 // Properties that cannot be modified 3302 // 3303 3304 params.dataRegionName = region.name; 3305 params.schemaName = region.schemaName; 3306 params.viewName = region.viewName; 3307 params.reportId = region.reportId; 3308 params.returnURL = window.location.href; 3309 params['webpart.name'] = 'Query'; 3310 3311 if (region.queryName) { 3312 params.queryName = region.queryName; 3313 } 3314 else if (region.sql) { 3315 params.sql = region.sql; 3316 } 3317 3318 var key = region.name + CONTAINER_FILTER_NAME; 3319 var cf = region.getContainerFilter.call(region); 3320 if (cf && !(key in params)) { 3321 params[key] = cf; 3322 } 3323 3324 return params; 3325 }; 3326 3327 var _updateFilter = function(region, filter, skipPrefixes) { 3328 var params = []; 3329 if (filter) { 3330 params.push([filter.getURLParameterName(region.name), filter.getURLParameterValue()]); 3331 } 3332 _setParameters(region, params, [OFFSET_PREFIX].concat(skipPrefixes)); 3333 }; 3334 3335 var _updateRequiresSelectionButtons = function(region, selectedCount) { 3336 3337 // update the 'select all on page' checkbox state 3338 _getAllRowSelectors(region).each(function() { 3339 if (region.isPageSelected.call(region)) { 3340 this.checked = true; 3341 this.indeterminate = false; 3342 } 3343 else if (region.selectedCount > 0) { 3344 // There are rows selected, but the are not visible on this page. 3345 this.checked = false; 3346 this.indeterminate = true; 3347 } 3348 else { 3349 this.checked = false; 3350 this.indeterminate = false; 3351 } 3352 }); 3353 3354 // If all rows have been selected (but not all rows are visible), show selection message 3355 if (region.totalRows && region.selectedCount == region.totalRows && !region.complete) { 3356 _showSelectMessage(region, 'All <span class="labkey-strong">' + region.totalRows + '</span> rows selected.'); 3357 } 3358 3359 // 10566: for javascript perf on IE stash the requires selection buttons 3360 if (!region._requiresSelectionButtons) { 3361 // escape ', ", and \ 3362 var escaped = region.name.replace(/('|"|\\)/g, "\\$1"); 3363 region._requiresSelectionButtons = $("a[labkey-requires-selection='" + escaped + "']"); 3364 } 3365 3366 region._requiresSelectionButtons.each(function() { 3367 var el = $(this); 3368 3369 // handle min-count 3370 var minCount = el.attr('labkey-requires-selection-min-count'); 3371 if (minCount) { 3372 minCount = parseInt(minCount); 3373 } 3374 if (minCount === undefined) { 3375 minCount = 1; 3376 } 3377 3378 // handle max-count 3379 var maxCount = el.attr('labkey-requires-selection-max-count'); 3380 if (maxCount) { 3381 maxCount = parseInt(maxCount); 3382 } 3383 3384 if (minCount <= selectedCount && (!maxCount || maxCount >= selectedCount)) { 3385 el.addClass('labkey-button').removeClass('labkey-disabled-button'); 3386 } 3387 else { 3388 el.addClass('labkey-disabled-button').removeClass('labkey-button'); 3389 } 3390 }); 3391 }; 3392 3393 var HeaderLock = function(region) { 3394 3395 var me = this, 3396 timeout; 3397 3398 var calculateHeaderPosition = function(recalcPosition) { 3399 calculateLockPosition(recalcPosition); 3400 onScroll(); 3401 }; 3402 3403 var calculateLockPosition = function(recalcPosition) { 3404 var el, s, src, i = 0; 3405 3406 for (; i < me.rowContent.length; i++) { 3407 src = $(me.firstRow[i]); 3408 el = $(me.rowContent[i]); 3409 3410 s = { 3411 width: src.width(), 3412 height: el.height() 3413 }; // note: width coming from data row not header 3414 3415 el.width(s.width); // 15420 3416 3417 $(me.rowSpacerContent[i]).height(s.height).width(s.width); 3418 } 3419 3420 if (recalcPosition === true) { 3421 me.hdrCoord = findPos(); 3422 } 3423 3424 me.hdrLocked = false; 3425 }; 3426 3427 var disable = function() { 3428 me.region._allowHeaderLock = false; 3429 3430 if (timeout) { 3431 clearTimeout(timeout); 3432 } 3433 3434 $(window).unbind('load', onResize); 3435 $(window).unbind('resize', onResize); 3436 $(window).unbind('scroll', onScroll); 3437 $(document).unbind('DOMNodeInserted', onResize); 3438 }; 3439 3440 var ensurePaginationVisible = function() { 3441 if (me.paginationEl) { 3442 // in case header locking is not on 3443 if (!me.region.headerLock() || !me.hdrCoord || me.hdrCoord.length == 0) { 3444 me.hdrCoord = findPos(); 3445 } 3446 3447 var measure = $('body').width() - me.hdrCoord[0]; 3448 if (measure < me.headerRow.width()) { 3449 me.paginationEl.width(measure); 3450 } 3451 } 3452 }; 3453 3454 /** 3455 * Returns an array of containing the following values: 3456 * [0] - X-coordinate of the top of the object relative to the offset parent. See Ext.Element.getXY() 3457 * [1] - Y-coordinate of the top of the object relative to the offset parent. See Ext.Element.getXY() 3458 * [2] - Y-coordinate of the bottom of the object. 3459 * [3] - The height of the header for this Data Region. This includes the button bar if it is present. 3460 * This method assumes interaction with the Header of the Data Region. 3461 */ 3462 var findPos = function() { 3463 var o, 3464 pos, 3465 curbottom, 3466 hdrOffset = 0; 3467 3468 if (me.includeHeader) { 3469 o = (me.hdrLocked ? me.headerSpacer : me.headerRow); 3470 hdrOffset = me.headerSpacer.height(); 3471 } 3472 else { 3473 o = (me.hdrLocked ? me.colHeaderRowSpacer : me.colHeaderRow); 3474 } 3475 3476 pos = o.offset(); 3477 curbottom = pos.top + me.table.height() - (o.height() * 2); 3478 3479 return [ pos.left, pos.top, curbottom, hdrOffset ]; 3480 }; 3481 3482 var onResize = function() { 3483 if (!me.table) { 3484 return; 3485 } 3486 3487 if (me.region.headerLock()) { 3488 if (timeout) { 3489 clearTimeout(timeout); 3490 } 3491 timeout = setTimeout(resizeTask, 110); 3492 } 3493 else { 3494 ensurePaginationVisible(); 3495 } 3496 }; 3497 3498 /** 3499 * WARNING: This function is called often. Performance implications for each line. 3500 * NOTE: window.pageYOffset and pageXOffset are not available in IE7-. For these document.documentElement.scrollTop 3501 * and document.documentElement.scrollLeft could be used. Additionally, position: fixed is not recognized by 3502 * IE7- and can be best approximated with position: absolute and explicit top/left. 3503 */ 3504 var onScroll = function() { 3505 var hrStyle, chrStyle; 3506 3507 // calculate Y scrolling 3508 if (window.pageYOffset >= me.hdrCoord[1] && window.pageYOffset < me.hdrCoord[2]) { 3509 // The header has reached the top of the window and needs to be locked 3510 var tWidth = me.table.width(); 3511 3512 hrStyle = { 3513 top: 0, 3514 position: 'fixed', 3515 'min-width': tWidth, 3516 'z-index': 9000 // 13229 3517 }; 3518 chrStyle = { 3519 top: me.hdrCoord[3], 3520 position: 'fixed', 3521 background: 'white', 3522 'min-width': tWidth, 3523 'box-shadow': '-2px 5px 5px #DCDCDC', 3524 'z-index': 9000 // 13229 3525 }; 3526 3527 me.headerSpacer.css('display', 'table-row'); 3528 me.colHeaderRowSpacer.css('display', 'table-row'); 3529 me.headerRowContent.css('min-width', tWidth - 3); 3530 me.hdrLocked = true; 3531 } 3532 else if (me.hdrLocked && window.pageYOffset >= me.hdrCoord[2]) { 3533 // The bottom of the Data Region is near the top of the window and the locked header 3534 // needs to start 'sliding' out of view. 3535 var top = me.hdrCoord[2] - window.pageYOffset; 3536 hrStyle = { top: top }; 3537 chrStyle = { top: (top + me.hdrCoord[3]) }; 3538 } 3539 else if (me.hdrLocked) { // only reset if the header is locked 3540 // the header should not be locked 3541 reset(false); 3542 } 3543 3544 // Calculate X Scrolling 3545 if (me.hdrLocked) { 3546 if (!hrStyle) { 3547 hrStyle = {}; 3548 } 3549 if (!chrStyle) { 3550 chrStyle = {}; 3551 } 3552 3553 hrStyle.left = me.hdrCoord[0] - window.pageXOffset; 3554 chrStyle.left = me.hdrCoord[0] - window.pageXOffset; 3555 } 3556 3557 if (hrStyle) { 3558 me.headerRow.css(hrStyle); 3559 } 3560 if (chrStyle) { 3561 me.colHeaderRow.css(chrStyle); 3562 } 3563 }; 3564 3565 /** 3566 * Adjusts the header styling to the best approximate of what the defaults are when the header is not locked 3567 */ 3568 var reset = function(recalc) { 3569 me.hdrLocked = false; 3570 me.headerRow.css({ 3571 top: 'auto', 3572 position: 'static', 3573 'min-width': 0 3574 }); 3575 me.headerRowContent.css('min-width', 0); 3576 me.colHeaderRow.css({ 3577 top: 'auto', 3578 position: 'static', 3579 'min-width': 0, 3580 'box-shadow': 'none' 3581 }); 3582 me.headerSpacer.hide(); 3583 me.headerSpacer.height(me.headerRow.height()); 3584 me.colHeaderRowSpacer.hide(); 3585 calculateHeaderPosition(recalc); 3586 }; 3587 3588 var resizeTask = function() { 3589 reset(true); 3590 ensurePaginationVisible(); 3591 }; 3592 3593 var validBrowser = function() { 3594 return true; // Ext.isIE9 || Ext.isIE10p || Ext.isWebKit || Ext.isGecko 3595 }; 3596 3597 // init 3598 if (!region.headerLock() || !validBrowser()) { 3599 region._allowHeaderLock = false; 3600 return; 3601 } 3602 3603 this.region = region; 3604 3605 // initialize constants 3606 this.headerRow = $('#' + region.domId + '-header-row'); 3607 if (!this.headerRow) { 3608 region._allowHeaderLock = false; 3609 return; 3610 } 3611 3612 this.table = $('#' + region.domId); 3613 this.headerRowContent = this.headerRow.children('td'); 3614 this.headerSpacer = $('#' + region.domId + '-header-row-spacer'); 3615 this.colHeaderRow = $('#' + region.domId + '-column-header-row'); 3616 this.colHeaderRowSpacer = $('#' + region.domId + '-column-header-row-spacer'); 3617 this.paginationEl = $('#' + region.domId + '-header'); 3618 3619 // check if the header row is being used 3620 this.includeHeader = this.headerRow.is(':visible'); // formerly isDisplayed() 3621 3622 // initialize row contents 3623 // Check if we have colHeaderRow and colHeaderRowSpacer - they won't be present if there was an SQLException 3624 // during query execution, so we didn't get column metadata back 3625 if (this.colHeaderRow) { 3626 this.rowContent = this.colHeaderRow.find('td.labkey-column-header'); 3627 } 3628 if (this.colHeaderRowSpacer) { 3629 this.rowSpacerContent = this.colHeaderRowSpacer.find('td.labkey-column-header'); 3630 } 3631 this.firstRow = this.table.find('tr.labkey-alternate-row').first().children('td'); 3632 3633 // performance degradation 3634 var tooManyColumns = this.rowContent.length > 100; 3635 var tooManyRows = (region.rowCount && region.rowCount > 1000); 3636 3637 if (tooManyColumns || tooManyRows) { 3638 region._allowHeaderLock = false; 3639 return; 3640 } 3641 3642 // If no data rows exist just turn off header locking 3643 if (this.firstRow.length == 0) { 3644 this.firstRow = this.table.find('tr.labkey-row').first().children('td'); 3645 if (this.firstRow.length == 0) { 3646 region._allowHeaderLock = false; 3647 return; 3648 } 3649 } 3650 3651 // initialize additional listeners 3652 $(window).one('load', onResize); 3653 $(window).on('resize', onResize); 3654 $(document).bind('DOMNodeInserted', onResize); // Issue #13121 3655 $(window).scroll(onScroll); 3656 3657 ensurePaginationVisible(); 3658 3659 // initialize panel listeners 3660 // 13669: customize view jumping when using drag/drop to reorder columns/filters/sorts 3661 // must manage DOMNodeInserted Listeners due to panels possibly dynamically adding elements to page 3662 region.on('afterpanelshow', function() { 3663 $(document).unbind('DOMNodeInserted', onResize); // suspend listener 3664 onResize(); 3665 }, this); 3666 3667 region.on('afterpanelhide', function() { 3668 $(document).bind('DOMNodeInserted', onResize); // resume listener 3669 onResize(); 3670 }, this); 3671 3672 this.hdrCoord = []; 3673 3674 reset(true); 3675 3676 // public methods 3677 return { 3678 disable: disable 3679 }; 3680 }; 3681 3682 // 3683 // LOADER 3684 // 3685 LABKEY.DataRegion.create = function(config) { 3686 3687 var region = LABKEY.DataRegions[config.name]; 3688 3689 if (region) { 3690 // region already exists, update properties 3691 $.each(config, function(key, value) { 3692 region[key] = value; 3693 }); 3694 if (!config.view) { 3695 // when switching back to 'default' view, needs to clear region.view 3696 region.view = undefined; 3697 } 3698 region._init(config); 3699 } 3700 else { 3701 // instantiate a new region 3702 region = new LABKEY.DataRegion(config); 3703 LABKEY.DataRegions[region.name] = region; 3704 } 3705 3706 return region; 3707 }; 3708 3709 LABKEY.DataRegion.loadViewDesigner = function(cb, scope) { 3710 LABKEY.requiresExt4Sandbox(function() { 3711 LABKEY.requiresScript('internal/ViewDesigner', cb, scope); 3712 }); 3713 }; 3714 3715 LABKEY.DataRegion.getCustomViewEditableErrors = function(customView) { 3716 var errors = []; 3717 if (customView && !customView.editable) { 3718 errors.push("The view is read-only and cannot be edited."); 3719 } 3720 return errors; 3721 }; 3722 3723 LABKEY.DataRegion.registerPane = function(regionName, callback, scope) { 3724 var region = LABKEY.DataRegions[regionName]; 3725 if (region) { 3726 callback.call(scope || region, region); 3727 return; 3728 } 3729 else if (!_paneCache[regionName]) { 3730 _paneCache[regionName] = []; 3731 } 3732 3733 _paneCache[regionName].push({cb: callback, scope: scope}); 3734 }; 3735 3736 LABKEY.DataRegion.selectAll = function(config) { 3737 var params = {}; 3738 if (!config.url) { 3739 // DataRegion doesn't have selectAllURL so generate url and query parameters manually 3740 config.url = LABKEY.ActionURL.buildURL('query', 'selectAll.api', config.containerPath); 3741 3742 config.dataRegionName = config.dataRegionName || 'query'; 3743 3744 params = LABKEY.Query.buildQueryParams( 3745 config.schemaName, 3746 config.queryName, 3747 config.filters, 3748 null, 3749 config.dataRegionName 3750 ); 3751 3752 if (config.viewName) 3753 params[config.dataRegionName + VIEWNAME_PREFIX] = config.viewName; 3754 3755 if (config.containerFilter) 3756 params.containerFilter = config.containerFilter; 3757 3758 if (config.selectionKey) 3759 params[config.dataRegionName + '.selectionKey'] = config.selectionKey; 3760 3761 $.each(config.parameters, function(propName, value) { 3762 params[config.dataRegionName + PARAM_PREFIX + propName] = value; 3763 }); 3764 3765 if (config.ignoreFilter) { 3766 params[config.dataRegionName + '.ignoreFilter'] = true; 3767 } 3768 3769 // NOTE: ignore maxRows, showRows, and offset 3770 } 3771 3772 LABKEY.Ajax.request({ 3773 url: config.url, 3774 method: 'POST', 3775 params: params, 3776 success: LABKEY.Utils.getCallbackWrapper(LABKEY.Utils.getOnSuccess(config), config.scope), 3777 failure: LABKEY.Utils.getCallbackWrapper(LABKEY.Utils.getOnFailure(config), config.scope, true) 3778 }); 3779 }; 3780 3781 /** 3782 * Static method to add or remove items from the selection for a given {@link #selectionKey}. 3783 * 3784 * @param config A configuration object with the following properties: 3785 * @param {String} config.selectionKey See {@link #selectionKey}. 3786 * @param {Array} config.ids Array of primary key ids for each row to select/unselect. 3787 * @param {Boolean} config.checked If true, the ids will be selected, otherwise unselected. 3788 * @param {Function} config.success The function to be called upon success of the request. 3789 * The callback will be passed the following parameters: 3790 * <ul> 3791 * <li><b>data:</b> an object with the property 'count' to indicate the updated selection count. 3792 * <li><b>response:</b> The XMLHttpResponse object</li> 3793 * </ul> 3794 * @param {Function} [config.failure] The function to call upon error of the request. 3795 * The callback will be passed the following parameters: 3796 * <ul> 3797 * <li><b>errorInfo:</b> an object containing detailed error information (may be null)</li> 3798 * <li><b>response:</b> The XMLHttpResponse object</li> 3799 * </ul> 3800 * @param {Object} [config.scope] An optional scoping object for the success and error callback functions (default to this). 3801 * @param {string} [config.containerPath] An alternate container path. If not specified, the current container path will be used. 3802 * 3803 * @see LABKEY.DataRegion#getSelected 3804 * @see LABKEY.DataRegion#clearSelected 3805 */ 3806 LABKEY.DataRegion.setSelected = function(config) { 3807 // Formerly LABKEY.DataRegion.setSelected 3808 var url = LABKEY.ActionURL.buildURL("query", "setSelected.api", config.containerPath, 3809 { 'key': config.selectionKey, 'checked': config.checked }); 3810 3811 LABKEY.Ajax.request({ 3812 url: url, 3813 method: "POST", 3814 params: { id: config.ids || config.id }, 3815 scope: config.scope, 3816 success: LABKEY.Utils.getCallbackWrapper(LABKEY.Utils.getOnSuccess(config), config.scope), 3817 failure: LABKEY.Utils.getCallbackWrapper(LABKEY.Utils.getOnFailure(config), config.scope, true) 3818 }); 3819 }; 3820 3821 /** 3822 * Static method to clear all selected items for a given {@link #selectionKey}. 3823 * 3824 * @param config A configuration object with the following properties: 3825 * @param {String} config.selectionKey See {@link #selectionKey}. 3826 * @param {Function} config.success The function to be called upon success of the request. 3827 * The callback will be passed the following parameters: 3828 * <ul> 3829 * <li><b>data:</b> an object with the property 'count' of 0 to indicate an empty selection. 3830 * <li><b>response:</b> The XMLHttpResponse object</li> 3831 * </ul> 3832 * @param {Function} [config.failure] The function to call upon error of the request. 3833 * The callback will be passed the following parameters: 3834 * <ul> 3835 * <li><b>errorInfo:</b> an object containing detailed error information (may be null)</li> 3836 * <li><b>response:</b> The XMLHttpResponse object</li> 3837 * </ul> 3838 * @param {Object} [config.scope] An optional scoping object for the success and error callback functions (default to this). 3839 * @param {string} [config.containerPath] An alternate container path. If not specified, the current container path will be used. 3840 * 3841 * @see LABKEY.DataRegion#setSelected 3842 * @see LABKEY.DataRegion#getSelected 3843 */ 3844 LABKEY.DataRegion.clearSelected = function(config) { 3845 var url = LABKEY.ActionURL.buildURL('query', 'clearSelected.api', config.containerPath, 3846 { 'key': config.selectionKey }); 3847 3848 LABKEY.Ajax.request({ url: url }); 3849 }; 3850 3851 /** 3852 * Static method to get all selected items for a given {@link #selectionKey}. 3853 * 3854 * @param config A configuration object with the following properties: 3855 * @param {String} config.selectionKey See {@link #selectionKey}. 3856 * @param {Function} config.success The function to be called upon success of the request. 3857 * The callback will be passed the following parameters: 3858 * <ul> 3859 * <li><b>data:</b> an object with the property 'selected' that is an array of the primary keys for the selected rows. 3860 * <li><b>response:</b> The XMLHttpResponse object</li> 3861 * </ul> 3862 * @param {Function} [config.failure] The function to call upon error of the request. 3863 * The callback will be passed the following parameters: 3864 * <ul> 3865 * <li><b>errorInfo:</b> an object containing detailed error information (may be null)</li> 3866 * <li><b>response:</b> The XMLHttpResponse object</li> 3867 * </ul> 3868 * @param {Object} [config.scope] An optional scoping object for the success and error callback functions (default to this). 3869 * @param {string} [config.containerPath] An alternate container path. If not specified, the current container path will be used. 3870 * 3871 * @see LABKEY.DataRegion#setSelected 3872 * @see LABKEY.DataRegion#clearSelected 3873 */ 3874 LABKEY.DataRegion.getSelected = function(config) { 3875 var url = LABKEY.ActionURL.buildURL('query', 'getSelected.api', config.containerPath, 3876 { 'key': config.selectionKey }); 3877 3878 LABKEY.Ajax.request({ 3879 url: url, 3880 success: LABKEY.Utils.getCallbackWrapper(LABKEY.Utils.getOnSuccess(config), config.scope), 3881 failure: LABKEY.Utils.getCallbackWrapper(LABKEY.Utils.getOnFailure(config), config.scope, true) 3882 }); 3883 }; 3884 3885 /** 3886 * MessageArea wraps the display of messages in a DataRegion. 3887 * @param dataRegion - The dataregion that the MessageArea will bind itself to. 3888 * @param {String[]} [messages] - An initial messages object containing mappings of 'part' to 'msg' 3889 * @constructor 3890 */ 3891 var MessageArea = function(dataRegion, messages) { 3892 this.bindRegion(dataRegion); 3893 3894 if (messages) { 3895 this.setMessages(messages); 3896 } 3897 }; 3898 3899 var MsgProto = MessageArea.prototype; 3900 3901 MsgProto.bindRegion = function(region) { 3902 this.parentDataRegion = region; 3903 this.parentSel = '#' + region.domId + '-msgbox'; 3904 }; 3905 3906 MsgProto.toJSON = function() { 3907 return this.parts; 3908 }; 3909 3910 MsgProto.addMessage = function(msg, part, append) { 3911 part = part || 'info'; 3912 3913 var p = part.toLowerCase(); 3914 if (append && this.parts.hasOwnProperty(p)) 3915 { 3916 this.parts[p] += msg; 3917 this.render(p, msg); 3918 } 3919 else 3920 { 3921 this.parts[p] = msg; 3922 this.render(p); 3923 } 3924 3925 this.expand(); 3926 }; 3927 3928 MsgProto.getMessage = function(part) { 3929 return this.parts[part.toLowerCase()]; 3930 }; 3931 3932 MsgProto.hasMessage = function(part) { 3933 return this.getMessage(part) !== undefined; 3934 }; 3935 3936 MsgProto.hasContent = function() { 3937 return this.parts && Object.keys(this.parts).length > 0; 3938 }; 3939 3940 MsgProto.removeAll = function() { 3941 this.parts = {}; 3942 this.render(); 3943 }; 3944 3945 MsgProto.removeMessage = function(part) { 3946 var p = part.toLowerCase(); 3947 if (this.parts.hasOwnProperty(p)) { 3948 this.parts[p] = undefined; 3949 this.render(); 3950 } 3951 }; 3952 3953 MsgProto.setMessages = function(messages) { 3954 if (LABKEY.Utils.isObject(messages)) { 3955 this.parts = messages; 3956 } 3957 else { 3958 this.parts = {}; 3959 } 3960 }; 3961 3962 MsgProto.getParent = function() { 3963 var parent = $(this.parentSel); 3964 3965 // ensure container div is present 3966 if (parent.find('div.dataregion_msgbox_ct').length == 0) { 3967 parent.find('td.labkey-dataregion-msgbox').append('<div class="dataregion_msgbox_ct"></div>'); 3968 } 3969 3970 return parent; 3971 }; 3972 3973 MsgProto.render = function(partToUpdate, appendMsg) { 3974 var parentCt = this.getParent().find('.dataregion_msgbox_ct'), 3975 hasMsg = false, msgCls = '', 3976 partCls, partEl, 3977 me = this; 3978 3979 $.each(this.parts, function(part, msg) { 3980 partCls = 'labkey-dataregion-msg-part-' + part; 3981 3982 if (msg) { 3983 3984 partEl = parentCt.find('.' + partCls); 3985 if (partEl.length == 0) { 3986 3987 msgCls = 'labkey-dataregion-msg ' + partCls + (hasMsg ? ' labkey-dataregion-msg-sep' : ''); 3988 parentCt.append('<div class="' + msgCls + '">' + msg + '</div>'); 3989 } 3990 else if (partToUpdate != undefined && partToUpdate == part) { 3991 3992 if (appendMsg != undefined) 3993 partEl.append(appendMsg); 3994 else 3995 partEl.html(msg) 3996 } 3997 3998 hasMsg = true; 3999 } 4000 else { 4001 parentCt.find('.' + partCls).remove(); 4002 delete me.parts[part]; 4003 } 4004 }); 4005 4006 if (hasMsg) { 4007 this.show(); 4008 $(this).trigger('rendermsg', [this, this.parts]); 4009 } 4010 else { 4011 this.hide(); 4012 parentCt.html(''); 4013 } 4014 }; 4015 4016 MsgProto.expand = function() { 4017 if (this.isVisible()) { 4018 this.getParent().find('.labkey-dataregion-msg').show(); 4019 4020 var toggle = this.getToggleEl(); 4021 toggle.removeClass('fa-plus'); 4022 toggle.addClass('fa-minus'); 4023 toggle.prop('title', 'Collapse message'); 4024 4025 _getHeaderSelector(this.parentDataRegion).trigger('resize'); 4026 } 4027 }; 4028 4029 MsgProto.collapse = function() { 4030 if (this.isVisible()) { 4031 this.getParent().find('.labkey-dataregion-msg').hide(); 4032 4033 var toggle = this.getToggleEl(); 4034 toggle.removeClass('fa-minus'); 4035 toggle.addClass('fa-plus'); 4036 toggle.prop('title', 'Expand message'); 4037 4038 _getHeaderSelector(this.parentDataRegion).trigger('resize'); 4039 } 4040 }; 4041 4042 MsgProto.getToggleEl = function() { 4043 return this.getParent().find('.labkey-dataregion-msg-toggle'); 4044 }; 4045 4046 MsgProto.show = function() { this.getParent().show(); }; 4047 MsgProto.hide = function() { this.getParent().hide(); }; 4048 MsgProto.isVisible = function() { return $(this.parentSel + ':visible').length > 0; }; 4049 MsgProto.find = function(selector) { 4050 return this.getParent().find('.dataregion_msgbox_ct').find(selector); 4051 }; 4052 MsgProto.on = function(evt, callback, scope) { $(this).bind(evt, $.proxy(callback, scope)); }; 4053 4054 /** 4055 * @description Constructs a LABKEY.QueryWebPart class instance 4056 * @class The LABKEY.QueryWebPart simplifies the task of dynamically adding a query web part to your page. Please use 4057 * this class for adding query web parts to a page instead of {@link LABKEY.WebPart}, 4058 * which can be used for other types of web parts. 4059 * <p>Additional Documentation: 4060 * <ul> 4061 * <li><a href= "https://www.labkey.org/wiki/home/Documentation/page.view?name=webPartConfig"> 4062 * Web Part Configuration Properties</a></li> 4063 * <li><a href="https://www.labkey.org/wiki/home/Documentation/page.view?name=findNames"> 4064 * How To Find schemaName, queryName & viewName</a></li> 4065 * <li><a href="https://www.labkey.org/wiki/home/Documentation/page.view?name=javascriptTutorial">LabKey JavaScript API Tutorial</a> and 4066 * <a href="https://www.labkey.org/wiki/home/Study/demo/page.view?name=reagentRequest">Demo</a></li> 4067 * <li><a href="https://www.labkey.org/wiki/home/Documentation/page.view?name=labkeySql"> 4068 * LabKey SQL Reference</a></li> 4069 * </ul> 4070 * </p> 4071 * @constructor 4072 * @param {Object} config A configuration object with the following possible properties: 4073 * @param {String} config.schemaName The name of the schema the web part will query. 4074 * @param {String} config.queryName The name of the query within the schema the web part will select and display. 4075 * @param {String} [config.viewName] the name of a saved view you wish to display for the given schema and query name. 4076 * @param {String} [config.reportId] the report id of a saved report you wish to display for the given schema and query name. 4077 * @param {Mixed} [config.renderTo] The element id, DOM element, or Ext element inside of which the part should be rendered. This is typically a <div>. 4078 * If not supplied in the configuration, you must call the render() method to render the part into the page. 4079 * @param {String} [config.errorType] A parameter to specify how query parse errors are returned. (default 'html'). Valid 4080 * values are either 'html' or 'json'. If 'html' is specified the error will be rendered to an HTML view, if 'json' is specified 4081 * the errors will be returned to the callback handlers as an array of objects named 'parseErrors' with the following properties: 4082 * <ul> 4083 * <li><b>msg</b>: The error message.</li> 4084 * <li><b>line</b>: The line number the error occurred at (optional).</li> 4085 * <li><b>col</b>: The column number the error occurred at (optional).</li> 4086 * <li><b>errorStr</b>: The line from the source query that caused the error (optional).</li> 4087 * </ul> 4088 * @param {String} [config.sql] A SQL query that can be used instead of an existing schema name/query name combination. 4089 * @param {Object} [config.metadata] Metadata that can be applied to the properties of the table fields. Currently, this option is only 4090 * available if the query has been specified through the config.sql option. For full documentation on 4091 * available properties, see <a href="https://www.labkey.org/download/schema-docs/xml-schemas/schemas/tableInfo_xsd/schema-summary.html">LabKey XML Schema Reference</a>. 4092 * This object may contain the following properties: 4093 * <ul> 4094 * <li><b>type</b>: The type of metadata being specified. Currently, only 'xml' is supported.</li> 4095 * <li><b>value</b>: The metadata XML value as a string. For example: <code>'<tables xmlns="http://labkey.org/data/xml"><table tableName="Announcement" tableDbType="NOT_IN_DB"><columns><column columnName="Title"><columnTitle>Custom Title</columnTitle></column></columns></table></tables>'</code></li> 4096 * </ul> 4097 * @param {String} [config.title] A title for the web part. If not supplied, the query name will be used as the title. 4098 * @param {String} [config.titleHref] If supplied, the title will be rendered as a hyperlink with this value as the href attribute. 4099 * @param {String} [config.buttonBarPosition] DEPRECATED--see config.buttonBar.position 4100 * @param {boolean} [config.allowChooseQuery] If the button bar is showing, whether or not it should be include a button 4101 * to let the user choose a different query. 4102 * @param {boolean} [config.allowChooseView] If the button bar is showing, whether or not it should be include a button 4103 * to let the user choose a different view. 4104 * @param {String} [config.detailsURL] Specify or override the default details URL for the table with one of the form 4105 * "/controller/action.view?id=${RowId}" or "org.labkey.package.MyController$ActionAction.class?id=${RowId}" 4106 * @param {boolean} [config.showDetailsColumn] If the underlying table has a details URL, show a column that renders a [details] link (default true). If true, the record selectors will be included regardless of the 'showRecordSelectors' config option. 4107 * @param {String} [config.updateURL] Specify or override the default updateURL for the table with one of the form 4108 * "/controller/action.view?id=${RowId}" or "org.labkey.package.MyController$ActionAction.class?id=${RowId}" 4109 * @param {boolean} [config.showUpdateColumn] If the underlying table has an update URL, show a column that renders an [edit] link (default true). 4110 * @param {String} [config.insertURL] Specify or override the default insert URL for the table with one of the form 4111 * "/controller/insertAction.view" or "org.labkey.package.MyController$InsertActionAction.class" 4112 * @param {String} [config.importURL] Specify or override the default bulk import URL for the table with one of the form 4113 * "/controller/importAction.view" or "org.labkey.package.MyController$ImportActionAction.class" 4114 * @param {String} [config.deleteURL] Specify or override the default delete URL for the table with one of the form 4115 * "/controller/action.view" or "org.labkey.package.MyController$ActionAction.class". The keys for the selected rows 4116 * will be included in the POST. 4117 * @param {boolean} [config.showInsertNewButton] If the underlying table has an insert URL, show an "Insert New" button in the button bar (default true). 4118 * @param {boolean} [config.showDeleteButton] Show a "Delete" button in the button bar (default true). 4119 * @param {boolean} [config.showReports] If true, show reports on the Views menu (default true). 4120 * @param {boolean} [config.showExportButtons] Show the export button menu in the button bar (default true). 4121 * @param {boolean} [config.showBorders] Render the table with borders (default true). 4122 * @param {boolean} [config.showSurroundingBorder] Render the table with a surrounding border (default true). 4123 * @param {boolean} [config.showRecordSelectors] Render the select checkbox column (default undefined, meaning they will be shown if the query is updatable by the current user). 4124 * If 'showDeleteButton' is true, the checkboxes will be included regardless of the 'showRecordSelectors' config option. 4125 * @param {boolean} [config.showPagination] Show the pagination links and count (default true). 4126 * @param {boolean} [config.shadeAlternatingRows] Shade every other row with a light gray background color (default true). 4127 * @param {boolean} [config.suppressRenderErrors] If true, no alert will appear if there is a problem rendering the QueryWebpart. This is most often encountered if page configuration changes between the time when a request was made and the content loads. Defaults to false. 4128 * @param {Object} [config.buttonBar] Button bar configuration. This object may contain any of the following properties: 4129 * <ul> 4130 * <li><b>position</b>: Configures where the button bar will appear with respect to the data grid: legal values are 'top', 'bottom', 'none', or 'both'. Default is 'both'.</li> 4131 * <li><b>includeStandardButtons</b>: If true, all standard buttons not specifically mentioned in the items array will be included at the end of the button bar. Default is false.</li> 4132 * <li><b>items</b>: An array of button bar items. Each item may be either a reference to a standard button, or a new button configuration. 4133 * to reference standard buttons, use one of the properties on {@link #standardButtons}, or simply include a string 4134 * that matches the button's caption. To include a new button configuration, create an object with the following properties: 4135 * <ul> 4136 * <li><b>text</b>: The text you want displayed on the button (aka the caption).</li> 4137 * <li><b>url</b>: The URL to navigate to when the button is clicked. You may use LABKEY.ActionURL to build URLs to controller actions. 4138 * Specify this or a handler function, but not both.</li> 4139 * <li><b>handler</b>: A reference to the JavaScript function you want called when the button is clicked.</li> 4140 * <li><b>permission</b>: Optional. Permission that the current user must possess to see the button. 4141 * Valid options are 'READ', 'INSERT', 'UPDATE', 'DELETE', and 'ADMIN'. 4142 * Default is 'READ' if permissionClass is not specified.</li> 4143 * <li><b>permissionClass</b>: Optional. If permission (see above) is not specified, the fully qualified Java class 4144 * name of the permission that the user must possess to view the button.</li> 4145 * <li><b>requiresSelection</b>: A boolean value (true/false) indicating whether the button should only be enabled when 4146 * data rows are checked/selected.</li> 4147 * <li><b>items</b>: To create a drop-down menu button, set this to an array of menu item configurations. 4148 * Each menu item configuration can specify any of the following properties: 4149 * <ul> 4150 * <li><b>text</b>: The text of the menu item.</li> 4151 * <li><b>handler</b>: A reference to the JavaScript function you want called when the menu item is clicked.</li> 4152 * <li><b>icon</b>: A url to an image to use as the menu item's icon.</li> 4153 * <li><b>items</b>: An array of sub-menu item configurations. Used for fly-out menus.</li> 4154 * </ul> 4155 * </li> 4156 * </ul> 4157 * </li> 4158 * </ul> 4159 * @param {String} [config.sort] A base sort order to use. This is a comma-separated list of column names, each of 4160 * which may have a - prefix to indicate a descending sort. It will be treated as the final sort, after any that the user 4161 * has defined in a custom view or through interacting with the grid column headers. 4162 * @param {String} [config.removeableSort] An additional sort order to use. This is a comma-separated list of column names, each of 4163 * which may have a - prefix to indicate a descending sort. It will be treated as the first sort, before any that the user 4164 * has defined in a custom view or through interacting with the grid column headers. 4165 * @param {Array} [config.filters] A base set of filters to apply. This should be an array of {@link LABKEY.Filter} objects 4166 * each of which is created using the {@link LABKEY.Filter.create} method. These filters cannot be removed by the user 4167 * interacting with the UI. 4168 * For compatibility with the {@link LABKEY.Query} object, you may also specify base filters using config.filterArray. 4169 * @param {Array} [config.removeableFilters] A set of filters to apply. This should be an array of {@link LABKEY.Filter} objects 4170 * each of which is created using the {@link LABKEY.Filter.create} method. These filters can be modified or removed by the user 4171 * interacting with the UI. 4172 * @param {Object} [config.parameters] Map of name (string)/value pairs for the values of parameters if the SQL 4173 * references underlying queries that are parameterized. For example, the following passes two parameters to the query: {'Gender': 'M', 'CD4': '400'}. 4174 * The parameters are written to the request URL as follows: query.param.Gender=M&query.param.CD4=400. For details on parameterized SQL queries, see 4175 * <a href="https://www.labkey.org/wiki/home/Documentation/page.view?name=paramsql">Parameterized SQL Queries</a>. 4176 * @param {Array} [config.aggregates] An array of aggregate definitions. The objects in this array should have the properties: 4177 * <ul> 4178 * <li><b>column:</b> The name of the column to be aggregated.</li> 4179 * <li><b>type:</b> The aggregate type (see {@link LABKEY.AggregateTypes})</li> 4180 * <li><b>label:</b> Optional label used when rendering the aggregate row. 4181 * </ul> 4182 * @param {String} [config.showRows] Either 'paginated' (the default) 'selected', 'unselected', 'all', or 'none'. 4183 * When 'paginated', the maxRows and offset parameters can be used to page through the query's result set rows. 4184 * When 'selected' or 'unselected' the set of rows selected or unselected by the user in the grid view will be returned. 4185 * You can programmatically get and set the selection using the {@link LABKEY.DataRegion.setSelected} APIs. 4186 * Setting <code>config.maxRows</code> to -1 is the same as 'all' 4187 * and setting <code>config.maxRows</code> to 0 is the same as 'none'. 4188 * @param {Integer} [config.maxRows] The maximum number of rows to return from the server (defaults to 100). 4189 * If you want to return all possible rows, set this config property to -1. 4190 * @param {Integer} [config.offset] The index of the first row to return from the server (defaults to 0). 4191 * Use this along with the maxRows config property to request pages of data. 4192 * @param {String} [config.dataRegionName] The name to be used for the data region. This should be unique within 4193 * the set of query views on the page. If not supplied, a unique name is generated for you. 4194 * @param {String} [config.linkTarget] The name of a browser window/tab in which to open URLs rendered in the 4195 * QueryWebPart. If not supplied, links will generally be opened in the same browser window/tab where the QueryWebPart. 4196 * @param {String} [config.frame] The frame style to use for the web part. This may be one of the following: 4197 * 'div', 'portal', 'none', 'dialog', 'title', 'left-nav'. 4198 * @param {String} [config.showViewPanel] Open the customize view panel after rendering. The value of this option can be "true" or one of "ColumnsTab", "FilterTab", or "SortTab". 4199 * @param {String} [config.bodyClass] A CSS style class that will be added to the enclosing element for the web part. 4200 * @param {Function} [config.success] A function to call after the part has been rendered. It will be passed two arguments: 4201 * <ul> 4202 * <li><b>dataRegion:</b> the LABKEY.DataRegion object representing the rendered QueryWebPart</li> 4203 * <li><b>request:</b> the XMLHTTPRequest that was issued to the server</li> 4204 * </ul> 4205 * @param {Function} [config.failure] A function to call if the request to retrieve the content fails. It will be passed three arguments: 4206 * <ul> 4207 * <li><b>json:</b> JSON object containing the exception.</li> 4208 * <li><b>response:</b> The XMLHttpRequest object containing the response data.</li> 4209 * <li><b>options:</b> The parameter to the request call.</li> 4210 * </ul> 4211 * @param {Object} [config.scope] An object to use as the callback function's scope. Defaults to this. 4212 * @param {int} [config.timeout] A timeout for the AJAX call, in milliseconds. Default is 30000 (30 seconds). 4213 * @param {String} [config.containerPath] The container path in which the schema and query name are defined. If not supplied, the current container path will be used. 4214 * @param {String} [config.containerFilter] One of the values of {@link LABKEY.Query.containerFilter} that sets the scope of this query. If not supplied, the current folder will be used. 4215 * @example 4216 * <div id='queryTestDiv1'/> 4217 * <script type="text/javascript"> 4218 var qwp1 = new LABKEY.QueryWebPart({ 4219 4220 renderTo: 'queryTestDiv1', 4221 title: 'My Query Web Part', 4222 schemaName: 'lists', 4223 queryName: 'People', 4224 buttonBarPosition: 'none', 4225 aggregates: [ 4226 {column: 'First', type: LABKEY.AggregateTypes.COUNT, label: 'Total People'}, 4227 {column: 'Age', type: LABKEY.AggregateTypes.MEAN} 4228 ], 4229 filters: [ 4230 LABKEY.Filter.create('Last', 'Flintstone') 4231 ], 4232 sort: '-Last' 4233 }); 4234 4235 //note that you may also register for the 'render' event 4236 //instead of using the success config property. 4237 //registering for events is done using Ext event registration. 4238 //Example: 4239 qwp1.on("render", onRender); 4240 function onRender() 4241 { 4242 //...do something after the part has rendered... 4243 } 4244 4245 /////////////////////////////////////// 4246 // Custom Button Bar Example 4247 4248 var qwp1 = new LABKEY.QueryWebPart({ 4249 renderTo: 'queryTestDiv1', 4250 title: 'My Query Web Part', 4251 schemaName: 'lists', 4252 queryName: 'People', 4253 buttonBar: { 4254 includeStandardButtons: true, 4255 items:[ 4256 LABKEY.QueryWebPart.standardButtons.views, 4257 {text: 'Test', url: LABKEY.ActionURL.buildURL('project', 'begin')}, 4258 {text: 'Test Script', onClick: "alert('Hello World!'); return false;"}, 4259 {text: 'Test Handler', handler: onTestHandler}, 4260 {text: 'Test Menu', items: [ 4261 {text: 'Item 1', handler: onItem1Handler}, 4262 {text: 'Fly Out', items: [ 4263 {text: 'Sub Item 1', handler: onItem1Handler} 4264 ]}, 4265 '-', //separator 4266 {text: 'Item 2', handler: onItem2Handler} 4267 ]}, 4268 LABKEY.QueryWebPart.standardButtons.exportRows 4269 ]} 4270 }); 4271 4272 function onTestHandler(dataRegion) 4273 { 4274 alert("onTestHandler called!"); 4275 return false; 4276 } 4277 4278 function onItem1Handler(dataRegion) 4279 { 4280 alert("onItem1Handler called!"); 4281 } 4282 4283 function onItem2Handler(dataRegion) 4284 { 4285 alert("onItem2Handler called!"); 4286 } 4287 4288 </script> 4289 */ 4290 LABKEY.QueryWebPart = function(config) { 4291 config._useQWPDefaults = true; 4292 return LABKEY.DataRegion.create(config); 4293 }; 4294 4295 if (!$.fn.tab) { 4296 // TAB CLASS DEFINITION 4297 // ==================== 4298 var Tab = function (element) { 4299 this.element = $(element); 4300 }; 4301 4302 Tab.VERSION = '3.3.6'; 4303 4304 Tab.TRANSITION_DURATION = 150; 4305 4306 Tab.prototype.show = function () { 4307 var $this = this.element; 4308 var $ul = $this.closest('ul:not(.dropdown-menu)'); 4309 var selector = $this.data('target'); 4310 4311 if (!selector) { 4312 selector = $this.attr('href'); 4313 selector = selector && selector.replace(/.*(?=#[^\s]*$)/, ''); // strip for ie7 4314 } 4315 4316 if ($this.parent('li').hasClass('active')) { 4317 return; 4318 } 4319 4320 var $previous = $ul.find('.active:last a'); 4321 var hideEvent = $.Event('hide.bs.tab', { 4322 relatedTarget: $this[0] 4323 }); 4324 var showEvent = $.Event('show.bs.tab', { 4325 relatedTarget: $previous[0] 4326 }); 4327 4328 $previous.trigger(hideEvent); 4329 $this.trigger(showEvent); 4330 4331 if (showEvent.isDefaultPrevented() || hideEvent.isDefaultPrevented()) { 4332 return; 4333 } 4334 4335 var $target = $(selector); 4336 4337 this.activate($this.closest('li'), $ul); 4338 this.activate($target, $target.parent(), function () { 4339 $previous.trigger({ 4340 type: 'hidden.bs.tab', 4341 relatedTarget: $this[0] 4342 }); 4343 $this.trigger({ 4344 type: 'shown.bs.tab', 4345 relatedTarget: $previous[0] 4346 }) 4347 }) 4348 }; 4349 4350 Tab.prototype.activate = function (element, container, callback) { 4351 var $active = container.find('> .active'); 4352 var transition = callback 4353 && $.support.transition 4354 && ($active.length && $active.hasClass('fade') || !!container.find('> .fade').length); 4355 4356 function next() { 4357 $active 4358 .removeClass('active') 4359 .find('> .dropdown-menu > .active') 4360 .removeClass('active') 4361 .end() 4362 .find('[data-toggle="tab"]') 4363 .attr('aria-expanded', false); 4364 4365 element 4366 .addClass('active') 4367 .find('[data-toggle="tab"]') 4368 .attr('aria-expanded', true); 4369 4370 if (transition) { 4371 element[0].offsetWidth; // reflow for transition 4372 element.addClass('in'); 4373 } 4374 else { 4375 element.removeClass('fade'); 4376 } 4377 4378 if (element.parent('.dropdown-menu').length) { 4379 element 4380 .closest('li.dropdown') 4381 .addClass('active') 4382 .end() 4383 .find('[data-toggle="tab"]') 4384 .attr('aria-expanded', true) 4385 } 4386 4387 callback && callback(); 4388 } 4389 4390 $active.length && transition ? 4391 $active.one('bsTransitionEnd', next).emulateTransitionEnd(Tab.TRANSITION_DURATION) : 4392 next(); 4393 4394 $active.removeClass('in'); 4395 }; 4396 4397 4398 // TAB PLUGIN DEFINITION 4399 // ===================== 4400 4401 function Plugin(option) { 4402 return this.each(function () { 4403 var $this = $(this); 4404 var data = $this.data('bs.tab'); 4405 4406 if (!data) $this.data('bs.tab', (data = new Tab(this))); 4407 if (typeof option == 'string') data[option]() 4408 }) 4409 } 4410 4411 var old = $.fn.tab; 4412 4413 $.fn.tab = Plugin; 4414 $.fn.tab.Constructor = Tab; 4415 4416 4417 // TAB NO CONFLICT 4418 // =============== 4419 4420 $.fn.tab.noConflict = function () { 4421 $.fn.tab = old; 4422 return this; 4423 }; 4424 4425 4426 // TAB DATA-API 4427 // ============ 4428 4429 var clickHandler = function (e) { 4430 e.preventDefault(); 4431 Plugin.call($(this), 'show'); 4432 }; 4433 4434 $(document) 4435 .on('click.bs.tab.data-api', '[data-toggle="tab"]', clickHandler) 4436 .on('click.bs.tab.data-api', '[data-toggle="pill"]', clickHandler) 4437 } 4438 4439 })(jQuery); 4440 4441 /** 4442 * A read-only object that exposes properties representing standard buttons shown in LabKey data grids. 4443 * These are used in conjunction with the buttonBar configuration. The following buttons are currently defined: 4444 * <ul> 4445 * <li>LABKEY.QueryWebPart.standardButtons.query</li> 4446 * <li>LABKEY.QueryWebPart.standardButtons.views</li> 4447 * <li>LABKEY.QueryWebPart.standardButtons.insertNew</li> 4448 * <li>LABKEY.QueryWebPart.standardButtons.deleteRows</li> 4449 * <li>LABKEY.QueryWebPart.standardButtons.exportRows</li> 4450 * <li>LABKEY.QueryWebPart.standardButtons.print</li> 4451 * <li>LABKEY.QueryWebPart.standardButtons.pageSize</li> 4452 * </ul> 4453 * @name standardButtons 4454 * @memberOf LABKEY.QueryWebPart# 4455 */ 4456 LABKEY.QueryWebPart.standardButtons = { 4457 query: 'query', 4458 views: 'grid views', 4459 insertNew: 'insert new', 4460 deleteRows: 'delete', 4461 exportRows: 'export', 4462 print: 'print', 4463 pageSize: 'paging' 4464 }; 4465 4466 /** 4467 * Requests the query web part content and renders it within the element identified by the renderTo parameter. 4468 * Note that you do not need to call this method explicitly if you specify a renderTo property on the config object 4469 * handed to the class constructor. If you do not specify renderTo in the config, then you must call this method 4470 * passing the id of the element in which you want the part rendered 4471 * @function 4472 * @param renderTo The id of the element in which you want the part rendered. 4473 */ 4474 4475 LABKEY.QueryWebPart.prototype.render = LABKEY.DataRegion.prototype.render; 4476 4477 /** 4478 * @returns {LABKEY.DataRegion} 4479 */ 4480 LABKEY.QueryWebPart.prototype.getDataRegion = LABKEY.DataRegion.prototype.getDataRegion; 4481 4482 LABKEY.AggregateTypes = { 4483 /** 4484 * Displays the sum of the values in the specified column 4485 */ 4486 SUM: 'sum', 4487 /** 4488 * Displays the mean of the values in the specified column 4489 */ 4490 MEAN: 'mean', 4491 /** 4492 * Displays the count of the non-blank values in the specified column 4493 */ 4494 COUNT: 'count', 4495 /** 4496 * Displays the maximum value from the specified column 4497 */ 4498 MIN: 'min', 4499 /** 4500 * Displays the minimum values from the specified column 4501 */ 4502 MAX: 'max', 4503 4504 /** 4505 * Deprecated 4506 */ 4507 AVG: 'mean' 4508 4509 // TODO how to allow premium module additions to aggregate types? 4510 };