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