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