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