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