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