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