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