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