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 an aggregate for a given column in the DataRegion query view.
2236      * @param viewName
2237      * @param colFieldKey
2238      * @param aggType
2239      */
2240     LABKEY.DataRegion.prototype.toggleAggregateForCustomView = function(viewName, colFieldKey, aggType) {
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 colAggregateTypes = [];
2249                     $.each(view.aggregates, function (index, existingAgg) {
2250                         if (existingAgg.fieldKey == colFieldKey)
2251                             colAggregateTypes.push(existingAgg.type);
2252                     });
2253 
2254                     var index = colAggregateTypes.indexOf(aggType);
2255                     if (index == -1)
2256                         colAggregateTypes.push(aggType);
2257                     else
2258                         colAggregateTypes.splice(index, 1);
2259 
2260                     view = _applyAggregatesToCustomView(view, colFieldKey, colAggregateTypes);
2261                     this._updateSessionCustomView(view, true);
2262                 }
2263             }
2264         }, null, this);
2265     };
2266 
2267     /**
2268      * Remove a column from the given DataRegion query view.
2269      * @param viewName
2270      * @param colFieldKey
2271      */
2272     LABKEY.DataRegion.prototype.removeColumn = function(viewName, colFieldKey) {
2273         this.getQueryDetails(function(queryDetails)
2274         {
2275             var view = _getViewFromQueryDetails(queryDetails, viewName);
2276             if (view != null)
2277             {
2278                 if (_queryDetailsContainsColumn(queryDetails, colFieldKey))
2279                 {
2280                     var colFieldKeys = $.map(view.columns, function (c) {
2281                         return c.fieldKey;
2282                     }),
2283                     fieldKeyIndex = colFieldKeys.indexOf(colFieldKey);
2284 
2285                     if (fieldKeyIndex > -1)
2286                     {
2287                         view.columns.splice(fieldKeyIndex, 1);
2288                         this._updateSessionCustomView(view, true);
2289                     }
2290                 }
2291             }
2292         }, null, this);
2293     };
2294 
2295     /**
2296      * Add the enabled analytics provider to the custom view definition based on the column fieldKey and provider name.
2297      * In addition, disable the column menu item if the column is visible in the grid.
2298      * @param viewName
2299      * @param colFieldKey
2300      * @param providerName
2301      */
2302     LABKEY.DataRegion.prototype.addAnalyticsProviderForCustomView = function(viewName, colFieldKey, providerName) {
2303         this.getQueryDetails(function(queryDetails)
2304         {
2305             var view = _getViewFromQueryDetails(queryDetails, viewName);
2306             if (view != null)
2307             {
2308                 if (_queryDetailsContainsColumn(queryDetails, colFieldKey))
2309                 {
2310                     var colProviderNames = [];
2311                     $.each(view.analyticsProviders, function (index, existingProvider) {
2312                         if (existingProvider.fieldKey == colFieldKey)
2313                             colProviderNames.push(existingProvider.name);
2314                     });
2315 
2316                     if (colProviderNames.indexOf(providerName) == -1)
2317                     {
2318                         view.analyticsProviders.push({
2319                             fieldKey: colFieldKey,
2320                             name: providerName
2321                         });
2322 
2323                         this._updateSessionCustomView(view, false);
2324                     }
2325 
2326                     var elementId = this.name + ':' + colFieldKey + ':analytics-' + providerName;
2327                     Ext4.each(Ext4.ComponentQuery.query('menuitem[elementId=' + elementId + ']'), function(menuItem) {
2328                         menuItem.disable();
2329                     });
2330                 }
2331             }
2332         }, null, this);
2333     };
2334 
2335     /**
2336      * Remove an enabled analytics provider from the custom view definition based on the column fieldKey and provider name.
2337      * In addition, enable the column menu item if the column is visible in the grid.
2338      * @param viewName
2339      * @param colFieldKey
2340      * @param providerName
2341      */
2342     LABKEY.DataRegion.prototype.removeAnalyticsProviderForCustomView = function(viewName, colFieldKey, providerName) {
2343         this.getQueryDetails(function(queryDetails)
2344         {
2345             var view = _getViewFromQueryDetails(queryDetails, viewName);
2346             if (view != null)
2347             {
2348                 if (_queryDetailsContainsColumn(queryDetails, colFieldKey))
2349                 {
2350                     var indexToRemove = null;
2351                     $.each(view.analyticsProviders, function (index, existingProvider) {
2352                         if (existingProvider.fieldKey == colFieldKey && existingProvider.name == providerName) {
2353                             indexToRemove = index;
2354                             return false;
2355                         }
2356                     });
2357 
2358                     if (indexToRemove != null) {
2359                         view.analyticsProviders.splice(indexToRemove, 1);
2360                         this._updateSessionCustomView(view, false);
2361                     }
2362 
2363                     var elementId = this.name + ':' + colFieldKey + ':analytics-' + providerName;
2364                     Ext4.each(Ext4.ComponentQuery.query('menuitem[elementId=' + elementId + ']'), function(menuItem) {
2365                         menuItem.enable();
2366                     });
2367                 }
2368             }
2369         }, null, this);
2370     };
2371 
2372     /**
2373      * @private
2374      */
2375     LABKEY.DataRegion.prototype._openFilter = function(columnName) {
2376         if (this._dialogLoaded) {
2377             new LABKEY.FilterDialog({
2378                 dataRegionName: this.name,
2379                 column: this.getColumn(columnName),
2380                 cacheFacetResults: false // could have changed on Ajax
2381             }).show();
2382         }
2383         else {
2384             LABKEY.requiresExt3ClientAPI(function() {
2385                 this._dialogLoaded = true;
2386                 new LABKEY.FilterDialog({
2387                     dataRegionName: this.name,
2388                     column: this.getColumn(columnName),
2389                     cacheFacetResults: false // could have changed on Ajax
2390                 }).show();
2391             }, this);
2392         }
2393     };
2394 
2395     LABKEY.DataRegion.prototype._updateSessionCustomView = function(customView, requiresRefresh) {
2396         var viewConfig = $.extend({}, customView, {
2397             shared: false,
2398             inherit: false,
2399             session: true
2400         });
2401 
2402         LABKEY.Query.saveQueryViews({
2403             containerPath: this.containerFilter,
2404             schemaName: this.schemaName,
2405             queryName: this.queryName,
2406             views: [viewConfig],
2407             scope: this,
2408             success: function(info) {
2409                 if (requiresRefresh) {
2410                     this.refresh();
2411                 }
2412                 else if (info.views.length == 1) {
2413                     this.view = info.views[0];
2414                     this._initCustomViews();
2415                 }
2416             }
2417         });
2418     };
2419 
2420     //
2421     // PRIVATE FUNCTIONS
2422     //
2423     var _applyOptionalParameters = function(region, params, optionalParams) {
2424         $.each(optionalParams, function(i, p) {
2425             if (LABKEY.Utils.isObject(p)) {
2426                 if (region[p.name] !== undefined) {
2427                     if (p.check && !p.check.call(region, region[p.name])) {
2428                         return;
2429                     }
2430                     if (p.prefix) {
2431                         params[region.name + '.' + p.name] = region[p.name];
2432                     }
2433                     else {
2434                         params[p.name] = region[p.name];
2435                     }
2436                 }
2437             }
2438             else if (p && region[p] !== undefined) {
2439                 params[p] = region[p];
2440             }
2441         });
2442     };
2443 
2444     var _alterSortString = function(region, current, fieldKey, direction /* optional */) {
2445         fieldKey = _resolveFieldKey(region, fieldKey);
2446 
2447         var columnName = fieldKey.toString(),
2448             newSorts = [];
2449 
2450         if (current != null) {
2451             var sorts = current.split(',');
2452             $.each(sorts, function(i, sort) {
2453                 if (sort.length > 0 && (sort != columnName) && (sort != SORT_ASC + columnName) && (sort != SORT_DESC + columnName)) {
2454                     newSorts.push(sort);
2455                 }
2456             });
2457         }
2458 
2459         if (direction == SORT_ASC) { // Easier to read without the encoded + on the URL...
2460             direction = '';
2461         }
2462 
2463         if (LABKEY.Utils.isString(direction)) {
2464             newSorts = [direction + columnName].concat(newSorts);
2465         }
2466 
2467         return newSorts.join(',');
2468     };
2469 
2470     var _beforeRowsChange = function(region, rowChangeEnum) {
2471         //var event = $.Event('beforeshowrowschange');
2472         //$(region).trigger(event, [region, rowChangeEnum]);
2473         //if (event.isDefaultPrevented()) {
2474         //    return false;
2475         //}
2476         return true;
2477     };
2478 
2479     var _buildQueryString = function(region, pairs) {
2480         if (!$.isArray(pairs)) {
2481             return '';
2482         }
2483 
2484         var queryParts = [], key, value;
2485 
2486         $.each(pairs, function(i, pair) {
2487             key = pair[0];
2488             value = pair.length > 1 ? pair[1] : undefined;
2489 
2490             queryParts.push(encodeURIComponent(key));
2491             if (LABKEY.Utils.isDefined(value)) {
2492 
2493                 if (LABKEY.Utils.isDate(value)) {
2494                     value = $.format.date(value, 'yyyy-MM-dd');
2495                     if (LABKEY.Utils.endsWith(value, 'Z')) {
2496                         value = value.substring(0, value.length - 1);
2497                     }
2498                 }
2499                 queryParts.push('=');
2500                 queryParts.push(encodeURIComponent(value));
2501             }
2502             queryParts.push('&');
2503         });
2504 
2505         if (queryParts.length > 0) {
2506             queryParts.pop();
2507         }
2508 
2509         return queryParts.join("");
2510     };
2511 
2512     var _chainSelectionCountCallback = function(region, config) {
2513 
2514         var success = LABKEY.Utils.getOnSuccess(config);
2515 
2516         // On success, update the current selectedCount on this DataRegion and fire the 'selectchange' event
2517         config.success = function(data) {
2518             region.selectionModified = true;
2519             region.selectedCount = data.count;
2520             _onSelectionChange(region);
2521 
2522             // Chain updateSelected with the user-provided success callback
2523             if ($.isFunction(success)) {
2524                 success.call(config.scope, data);
2525             }
2526         };
2527 
2528         return config;
2529     };
2530 
2531     var _convertRenderTo = function(region, renderTo) {
2532         if (renderTo) {
2533             if (LABKEY.Utils.isString(renderTo)) {
2534                 region.renderTo = renderTo;
2535             }
2536             else if (LABKEY.Utils.isString(renderTo.id)) {
2537                 region.renderTo = renderTo.id; // support 'Ext' elements
2538             }
2539             else {
2540                 throw 'Unsupported "renderTo"';
2541             }
2542         }
2543 
2544         return region;
2545     };
2546 
2547     var _deleteTimer;
2548 
2549     var _beforeViewDelete = function(region, revert) {
2550         _deleteTimer = setTimeout(function() {
2551             _deleteTimer = 0;
2552             region.showLoadingMessage(revert ? 'Reverting view...' : 'Deleting view...');
2553         }, 500);
2554     };
2555 
2556     var _onViewDelete = function(region, success, json) {
2557         if (_deleteTimer) {
2558             clearTimeout(_deleteTimer);
2559         }
2560 
2561         if (success) {
2562             region.removeMessage.call(region, 'customizeview');
2563             region.showSuccessMessage.call(region);
2564 
2565             // change view to either a shadowed view or the default view
2566             var config = { type: 'view' };
2567             if (json.viewName) {
2568                 config.viewName = json.viewName;
2569             }
2570             region.changeView.call(region, config);
2571         }
2572         else {
2573             region.removeMessage.call(region, 'customizeview');
2574             region.showErrorMessage.call(region, json.exception);
2575         }
2576     };
2577 
2578     // The view can be reverted without ViewDesigner present
2579     var _revertCustomView = function(region) {
2580         _beforeViewDelete(region, true);
2581 
2582         var config = {
2583             schemaName: region.schemaName,
2584             queryName: region.queryName,
2585             revert: true,
2586             success: function(json) {
2587                 _onViewDelete(region, true /* success */, json);
2588             },
2589             failure: function(json) {
2590                 _onViewDelete(region, false /* success */, json);
2591             }
2592         };
2593 
2594         if (region.viewName) {
2595             config.viewName = region.viewName;
2596         }
2597 
2598         LABKEY.Query.deleteQueryView(config);
2599     };
2600 
2601     var _getViewFromQueryDetails = function(queryDetails, viewName)
2602     {
2603         var matchingView = null;
2604 
2605         $.each(queryDetails.views, function(index, view)
2606         {
2607             if (view.name == viewName)
2608             {
2609                 matchingView = view;
2610                 return false;
2611             }
2612         });
2613 
2614         return matchingView;
2615     };
2616 
2617     var _queryDetailsContainsColumn = function(queryDetails, colFieldKey)
2618     {
2619         var keys = $.map(queryDetails.columns, function(c){ return c.fieldKey; }),
2620             exists = keys.indexOf(colFieldKey) > -1;
2621 
2622         if (!exists) {
2623             console.warn('Unable to find column in query: ' + colFieldKey);
2624         }
2625 
2626         return exists;
2627     };
2628 
2629     var _applyAggregatesToCustomView = function(customView, fieldKey, newAggregates)
2630     {
2631         // first, keep any existing custom view aggregates that don't match this fieldKey
2632         var aggregates = [];
2633         $.each(customView.aggregates, function(index, existingAgg)
2634         {
2635             if (existingAgg.fieldKey != fieldKey)
2636                 aggregates.push(existingAgg);
2637         });
2638 
2639         // then add on the aggregates for the fieldKey selected
2640         $.each(newAggregates, function(index, newAggType)
2641         {
2642             aggregates.push({fieldKey: fieldKey, type: newAggType});
2643         });
2644 
2645         customView.aggregates = aggregates;
2646 
2647         return customView;
2648     };
2649 
2650     var _getAllRowSelectors = function(region) {
2651         return _getFormSelector(region).find('.labkey-selectors input[type="checkbox"][name=".toggle"]');
2652     };
2653 
2654     var _getFormSelector = function(region) {
2655         var form = $('form#' + region.domId + '-form');
2656 
2657         // derived DataRegion's may not include the form id
2658         if (form.length == 0) {
2659             form = $('#' + region.domId).closest('form');
2660         }
2661 
2662         return form;
2663     };
2664 
2665     var _getRowSelectors = function(region) {
2666         return _getFormSelector(region).find('.labkey-selectors input[type="checkbox"][name=".select"]');
2667     };
2668 
2669     var _getHeaderSelector = function(region) {
2670         return $('#' + region.domId + '-header');
2671     };
2672 
2673     // Formerly, LABKEY.DataRegion.getParamValPairsFromString / LABKEY.DataRegion.getParamValPairs
2674     var _getParameters = function(region, skipPrefixSet /* optional */) {
2675 
2676         var params = [];
2677         var qString = region.requestURL;
2678 
2679         if (LABKEY.Utils.isString(qString) && qString.length > 0) {
2680 
2681             var qmIdx = qString.indexOf('?');
2682             if (qmIdx > -1) {
2683                 qString = qString.substring(qmIdx + 1);
2684             }
2685 
2686             if (qString.length > 1) {
2687                 var pairs = qString.split('&'), p, key,
2688                     LAST = '.lastFilter', lastIdx, skip = $.isArray(skipPrefixSet);
2689 
2690                 $.each(pairs, function(i, pair) {
2691                     p = pair.split('=', 2);
2692                     key = p[0] = decodeURIComponent(p[0]);
2693                     lastIdx = key.indexOf(LAST);
2694 
2695                     if (lastIdx > -1 && lastIdx == (key.length - LAST.length)) {
2696                         return;
2697                     }
2698                     else if (REQUIRE_NAME_PREFIX.hasOwnProperty(key)) {
2699                         // 26686: Black list known parameters, should be prefixed by region name
2700                         return;
2701                     }
2702 
2703                     var stop = false;
2704                     if (skip) {
2705                         $.each(skipPrefixSet, function(j, skipPrefix) {
2706                             if (LABKEY.Utils.isString(skipPrefix)) {
2707 
2708                                 // Special prefix that should remove all filters, but no other parameters
2709                                 if (skipPrefix.indexOf(ALL_FILTERS_SKIP_PREFIX) == (skipPrefix.length - 2)) {
2710                                     if (key.indexOf('~') > 0) {
2711                                         stop = true;
2712                                         return false;
2713                                     }
2714                                 }
2715                                 else if (key.indexOf(skipPrefix) == 0) {
2716                                     // only skip filters, parameters, and sorts
2717                                     if (key == skipPrefix ||
2718                                             key.indexOf('~') > 0 ||
2719                                             key.indexOf(PARAM_PREFIX) > 0 ||
2720                                             key == (skipPrefix + 'sort')) {
2721                                         stop = true;
2722                                         return false;
2723                                     }
2724                                 }
2725                             }
2726                         });
2727                     }
2728 
2729                     if (!stop) {
2730                         if (p.length > 1) {
2731                             p[1] = decodeURIComponent(p[1]);
2732                         }
2733                         params.push(p);
2734                     }
2735                 });
2736             }
2737         }
2738 
2739         return params;
2740     };
2741 
2742     /**
2743      * 
2744      * @param region
2745      * @param {boolean} [asString=false]
2746      * @private
2747      */
2748     var _getUserSort = function(region, asString) {
2749         var userSort = [],
2750             sortParam = region.getParameter(region.name + SORT_PREFIX);
2751 
2752         if (asString) {
2753             userSort = sortParam || '';
2754         }
2755         else {
2756             if (sortParam) {
2757                 var fieldKey, dir;
2758                 $.each(sortParam.split(','), function(i, sort) {
2759                     fieldKey = sort;
2760                     dir = SORT_ASC;
2761                     if (sort.charAt(0) == SORT_DESC) {
2762                         fieldKey = fieldKey.substring(1);
2763                         dir = SORT_DESC;
2764                     }
2765                     else if (sort.charAt(0) == SORT_ASC) {
2766                         fieldKey = fieldKey.substring(1);
2767                     }
2768                     userSort.push({fieldKey: fieldKey, dir: dir});
2769                 });
2770             }
2771         }
2772 
2773         return userSort;
2774     };
2775 
2776     var _buttonBind = function(region, cls, fn) {
2777         region.msgbox.find('.labkey-button' + cls).off('click').on('click', $.proxy(function() {
2778             fn.call(this);
2779         }, region));
2780     };
2781 
2782     var _onRenderMessageArea = function(region, parts) {
2783         var msgArea = region.msgbox;
2784         if (msgArea) {
2785             if (region.showRecordSelectors && parts['selection']) {
2786                 _buttonBind(region, '.select-all', region.selectAll);
2787                 _buttonBind(region, '.select-none', region.clearSelected);
2788                 _buttonBind(region, '.show-all', region.showAll);
2789                 _buttonBind(region, '.show-selected', region.showSelected);
2790                 _buttonBind(region, '.show-unselected', region.showUnselected);
2791             }
2792             else if (parts['customizeview']) {
2793                 _buttonBind(region, '.unsavedview-revert', function() { _revertCustomView(this); });
2794                 _buttonBind(region, '.unsavedview-edit', function() { this.showCustomizeView(undefined); });
2795                 _buttonBind(region, '.unsavedview-save', function() { _saveSessionCustomView(this); });
2796             }
2797         }
2798     };
2799 
2800     var _onSelectionChange = function(region) {
2801         $(region).trigger('selectchange', [region, region.selectedCount]);
2802         _updateRequiresSelectionButtons(region, region.selectedCount);
2803         LABKEY.Utils.signalWebDriverTest('dataRegionUpdate', region.selectedCount);
2804     };
2805 
2806     var _onViewSave = function(region, designer, savedViewsInfo, urlParameters) {
2807         if (savedViewsInfo && savedViewsInfo.views.length > 0) {
2808             region.hideCustomizeView.call(region);
2809             region.changeView.call(region, {
2810                 type: 'view',
2811                 viewName: savedViewsInfo.views[0].name
2812             }, urlParameters);
2813         }
2814     };
2815 
2816     var _removeParameters = function(region, skipPrefixes /* optional */) {
2817         return _setParameters(region, null, skipPrefixes);
2818     };
2819 
2820     var _resolveFieldKey = function(region, fieldKey) {
2821         var fk = fieldKey;
2822         if (!(fk instanceof LABKEY.FieldKey)) {
2823             fk = LABKEY.FieldKey.fromString('' + fk);
2824         }
2825         return fk;
2826     };
2827 
2828     var _saveSessionCustomView = function(region) {
2829         // Note: currently only will save session views. Future version could create a new view using url sort/filters.
2830         if (!(region.view && region.view.session)) {
2831             return;
2832         }
2833 
2834         // Get the canEditSharedViews permission and candidate targetContainers.
2835         var viewName = (region.view && region.view.name) || region.viewName || '';
2836 
2837         LABKEY.Query.getQueryDetails({
2838             schemaName: region.schemaName,
2839             queryName: region.queryName,
2840             viewName: viewName,
2841             initializeMissingView: false,
2842             success: function (json) {
2843                 // Display an error if there was an issue error getting the query details
2844                 if (json.exception) {
2845                     var viewSourceUrl = LABKEY.ActionURL.buildURL('query', 'viewQuerySource.view', null, {schemaName: this.schemaName, "query.queryName": this.queryName});
2846                     var msg = LABKEY.Utils.encodeHtml(json.exception) + "  <a target=_blank class='labkey-button' href='" + viewSourceUrl + "'>View Source</a>";
2847 
2848                     this.showErrorMessage.call(this, msg);
2849                     return;
2850                 }
2851 
2852                 _saveSessionShowPrompt(this, json);
2853             },
2854             scope: region
2855         });
2856     };
2857 
2858     var _saveSessionShowPrompt = function(region, queryDetails) {
2859         var config = Ext4.applyIf({
2860             allowableContainerFilters: region.allowableContainerFilters,
2861             targetContainers: queryDetails.targetContainers,
2862             canEditSharedViews: queryDetails.canEditSharedViews,
2863             canEdit: LABKEY.DataRegion.getCustomViewEditableErrors(config).length == 0,
2864             success: function (win, o) {
2865                 var timerId = setTimeout(function() {
2866                     timerId = 0;
2867                     Ext4.Msg.progress("Saving...", "Saving custom view...");
2868                 }, 500);
2869 
2870                 var jsonData = {
2871                     schemaName: region.schemaName,
2872                     "query.queryName": region.queryName,
2873                     "query.viewName": region.viewName,
2874                     newName: o.name,
2875                     inherit: o.inherit,
2876                     shared: o.shared
2877                 };
2878 
2879                 if (o.inherit) {
2880                     jsonData.containerPath = o.containerPath;
2881                 }
2882 
2883                 LABKEY.Ajax.request({
2884                     url: LABKEY.ActionURL.buildURL('query', 'saveSessionView', region.containerPath),
2885                     method: 'POST',
2886                     jsonData: jsonData,
2887                     callback: function() {
2888                         if (timerId > 0)
2889                             clearTimeout(timerId);
2890                         win.close();
2891                     },
2892                     success: function() {
2893                         region.showSuccessMessage.call(region);
2894                         region.changeView.call(region, {type: 'view', viewName: o.name});
2895                     },
2896                     failure: function(json) {
2897                         Ext4.Msg.alert('Error saving view', json.exception || json.statusText);
2898                     },
2899                     scope: region
2900                 });
2901             },
2902             scope: region
2903         }, region.view);
2904 
2905         LABKEY.DataRegion.loadViewDesigner(function() {
2906             LABKEY.internal.ViewDesigner.Designer.saveCustomizeViewPrompt(config);
2907         });
2908     };
2909 
2910     var _setParameter = function(region, param, value, skipPrefixes /* optional */) {
2911         _setParameters(region, [[param, value]], skipPrefixes);
2912     };
2913 
2914     var _setParameters = function(region, newParamValPairs, skipPrefixes /* optional */) {
2915 
2916         // prepend region name
2917         // e.g. ['.hello', '.goodbye'] becomes ['aqwp19.hello', 'aqwp19.goodbye']
2918         if ($.isArray(skipPrefixes)) {
2919             $.each(skipPrefixes, function(i, skip) {
2920                 if (skip && skip.indexOf(region.name + '.') !== 0) {
2921                     skipPrefixes[i] = region.name + skip;
2922                 }
2923             });
2924         }
2925 
2926         var param, value,
2927             params = _getParameters(region, skipPrefixes);
2928 
2929         if ($.isArray(newParamValPairs)) {
2930             $.each(newParamValPairs, function(i, newPair) {
2931                 if (!$.isArray(newPair)) {
2932                     throw new Error("DataRegion: _setParameters newParamValPairs improperly initialized. It is an array of arrays. You most likely passed in an array of strings.");
2933                 }
2934                 param = newPair[0];
2935                 value = newPair[1];
2936 
2937                 // Allow value to be null/undefined to support no-value filter types (Is Blank, etc)
2938                 if (LABKEY.Utils.isString(param) && param.length > 1) {
2939                     if (param.indexOf(region.name) !== 0) {
2940                         param = region.name + param;
2941                     }
2942 
2943                     params.push([param, value]);
2944                 }
2945             });
2946         }
2947 
2948         if (region.async) {
2949             _load(region, undefined, undefined, params);
2950         }
2951         else {
2952             region.setSearchString.call(region, region.name, _buildQueryString(region, params));
2953         }
2954     };
2955 
2956     var _showRows = function(region, showRowsEnum) {
2957         if (_beforeRowsChange(region, showRowsEnum)) {
2958 
2959             // clear sibling parameters, could we do this with events?
2960             this.maxRows = undefined;
2961             this.offset = 0;
2962 
2963             _setParameter(region, SHOW_ROWS_PREFIX, showRowsEnum, [OFFSET_PREFIX, MAX_ROWS_PREFIX, SHOW_ROWS_PREFIX]);
2964         }
2965     };
2966 
2967     var _showSelectMessage = function(region, msg) {
2968         if (region.showRecordSelectors) {
2969             if (region.totalRows && region.totalRows != region.selectedCount) {
2970                 msg += " <span class='labkey-button select-all'>Select All " + region.totalRows + " Rows</span>";
2971             }
2972 
2973             msg += " " + "<span class='labkey-button select-none'>Select None</span>";
2974             var showOpts = [];
2975             if (region.showRows != "all")
2976                 showOpts.push("<span class='labkey-button show-all'>Show All</span>");
2977             if (region.showRows != "selected")
2978                 showOpts.push("<span class='labkey-button show-selected'>Show Selected</span>");
2979             if (region.showRows != "unselected")
2980                 showOpts.push("<span class='labkey-button show-unselected'>Show Unselected</span>");
2981             msg += "  " + showOpts.join(" ");
2982         }
2983 
2984         // add the record selector message, the link handlers will get added after render in _onRenderMessageArea
2985         region.addMessage.call(region, msg, 'selection');
2986     };
2987 
2988     var _toggleAllRows = function(region, checked) {
2989         var ids = [];
2990 
2991         _getRowSelectors(region).each(function() {
2992             if (!this.disabled) {
2993                 this.checked = checked;
2994                 ids.push(this.value);
2995             }
2996         });
2997 
2998         _getAllRowSelectors(region).each(function() { this.checked = (checked == true)});
2999         return ids;
3000     };
3001 
3002     var _load = function(region, callback, scope, newParams) {
3003 
3004         var params = _getAsyncParams(region, newParams ? newParams : _getParameters(region));
3005         var jsonData = _getAsyncBody(region, params);
3006 
3007         // TODO: This should be done in _getAsyncParams, but is not since _getAsyncBody relies on it. Refactor it.
3008         // ensure SQL is not on the URL -- we allow any property to be pulled through when creating parameters.
3009         if (params.sql) {
3010             delete params.sql;
3011         }
3012 
3013         /**
3014          * The target jQuery element that will be either written to or replaced
3015          */
3016         var target;
3017 
3018         /**
3019          * Flag used to determine if we should replace target element (default) or write to the target contents
3020          * (used during QWP render for example)
3021          * @type {boolean}
3022          */
3023         var useReplace = true;
3024 
3025         /**
3026          * The string identifier for where the region will render. Mainly used to display useful messaging upon failure.
3027          * @type {string}
3028          */
3029         var renderEl;
3030 
3031         if (region.renderTo) {
3032             useReplace = false;
3033             renderEl = region.renderTo;
3034             target = $('#' + region.renderTo);
3035         }
3036         else if (!region.domId) {
3037             throw '"renderTo" must be specified either upon construction or when calling render()';
3038         }
3039         else {
3040             renderEl = region.domId;
3041             target = $('#' + region.domId);
3042 
3043             // attempt to find the correct node to render to...
3044             var form = _getFormSelector(region);
3045             if (form.length && form.parent('div').length) {
3046                 target = form.parent('div');
3047             }
3048             else {
3049                 // next best render target
3050                 throw 'unable to find a good target element. Perhaps this region is not using the standard renderer?'
3051             }
3052         }
3053 
3054         LABKEY.Ajax.request({
3055             timeout: (region.timeout == undefined) ? DEFAULT_TIMEOUT : region.timeout,
3056             url: LABKEY.ActionURL.buildURL('project', 'getWebPart.api', region.containerPath),
3057             method: 'POST',
3058             params: params,
3059             jsonData: jsonData,
3060             success: function(response) {
3061 
3062                 this.hidePanel(function() {
3063                     if (target.length) {
3064 
3065                         this.destroy();
3066 
3067                         LABKEY.Utils.loadAjaxContent(response, target, function() {
3068 
3069                             if ($.isFunction(callback)) {
3070                                 callback.call(scope);
3071                             }
3072 
3073                             if ($.isFunction(this._success)) {
3074                                 this._success.call(this.scope || this, this, response);
3075                             }
3076 
3077                             $(this).trigger('success', [this, response]);
3078 
3079                             this.RENDER_LOCK = true;
3080                             $(this).trigger('render', this);
3081                             this.RENDER_LOCK = false;
3082                         }, this, useReplace);
3083                     }
3084                     else {
3085                         // not finding element considered a failure
3086                         if ($.isFunction(this._failure)) {
3087                             this._failure.call(this.scope || this, response /* json */, response, undefined /* options */, target);
3088                         }
3089                         else if (!this.suppressRenderErrors) {
3090                             LABKEY.Utils.alert('Rendering Error', 'The element "' + renderEl + '" does not exist in the document. You may need to specify "renderTo".');
3091                         }
3092                     }
3093                 }, this);
3094             },
3095             failure: LABKEY.Utils.getCallbackWrapper(function(json, response, options) {
3096 
3097                 if (target.length) {
3098                     if ($.isFunction(this._failure)) {
3099                         this._failure.call(this.scope || this, json, response, options);
3100                     }
3101                     else if (this.errorType === 'html') {
3102                         if (useReplace) {
3103                             target.replaceWith('<div class="labkey-error">' + LABKEY.Utils.encodeHtml(json.exception) + '</div>');
3104                         }
3105                         else {
3106                             target.html('<div class="labkey-error">' + LABKEY.Utils.encodeHtml(json.exception) + '</div>');
3107                         }
3108                     }
3109                 }
3110                 else if (!this.suppressRenderErrors) {
3111                     LABKEY.Utils.alert('Rendering Error', 'The element "' + renderEl + '" does not exist in the document. You may need to specify "renderTo".');
3112                 }
3113             }, region, true),
3114             scope: region
3115         });
3116     };
3117 
3118     var _getAsyncBody = function(region, params) {
3119         var json = {};
3120 
3121         if (params.sql) {
3122             json.sql = params.sql;
3123         }
3124 
3125         _processButtonBar(region, json);
3126 
3127         // 10505: add non-removable sorts and filters to json (not url params).
3128         if (region.sort || region.filters || region.aggregates) {
3129             json.filters = {};
3130 
3131             if (region.filters) {
3132                 LABKEY.Filter.appendFilterParams(json.filters, region.filters, region.name);
3133             }
3134 
3135             if (region.sort) {
3136                 json.filters[region.dataRegionName + SORT_PREFIX] = region.sort;
3137             }
3138 
3139             if (region.aggregates) {
3140                 LABKEY.Filter.appendAggregateParams(json.filters, region.aggregates, region.name);
3141             }
3142         }
3143 
3144         if (region.metadata) {
3145             json.metadata = region.metadata;
3146         }
3147 
3148         return json;
3149     };
3150 
3151     var _processButtonBar = function(region, json) {
3152 
3153         var bar = region.buttonBar;
3154 
3155         if (bar && (bar.position || (bar.items && bar.items.length > 0))) {
3156             _processButtonBarItems(region, bar.items);
3157 
3158             // only attach if valid
3159             json.buttonBar = bar;
3160         }
3161     };
3162 
3163     var _processButtonBarItems = function(region, items) {
3164         if ($.isArray(items) && items.length > 0) {
3165             for (var i = 0; i < items.length; i++) {
3166                 var item = items[i];
3167 
3168                 if (item && $.isFunction(item.handler)) {
3169                     item.id = item.id || LABKEY.Utils.id();
3170                     // TODO: A better way? This exposed _onButtonClick isn't very awesome
3171                     item.onClick = "return LABKEY.DataRegions['" + region.name + "']._onButtonClick('" + item.id + "');";
3172                 }
3173 
3174                 if (item.items) {
3175                     _processButtonBarItems(region, item.items);
3176                 }
3177             }
3178         }
3179     };
3180 
3181     var _isFilter = function(region, parameter) {
3182         return parameter && parameter.indexOf(region.name + '.') === 0 && parameter.indexOf('~') > 0;
3183     };
3184 
3185     var _getAsyncParams = function(region, newParams) {
3186 
3187         var params = {};
3188         var name = region.name;
3189 
3190         //
3191         // Certain parameters are only included if the region is 'async'. These
3192         // were formerly a part of Query Web Part.
3193         //
3194         if (region.async) {
3195             params[name + '.async'] = true;
3196 
3197             if (LABKEY.Utils.isString(region.frame)) {
3198                 params['webpart.frame'] = region.frame;
3199             }
3200 
3201             if (LABKEY.Utils.isString(region.bodyClass)) {
3202                 params['webpart.bodyClass'] = region.bodyClass;
3203             }
3204 
3205             if (LABKEY.Utils.isString(region.title)) {
3206                 params['webpart.title'] = region.title;
3207             }
3208 
3209             if (LABKEY.Utils.isString(region.titleHref)) {
3210                 params['webpart.titleHref'] = region.titleHref;
3211             }
3212 
3213             _applyOptionalParameters(region, params, [
3214                 'allowChooseQuery',
3215                 'allowChooseView',
3216                 'allowHeaderLock',
3217                 'buttonBarPosition',
3218                 'detailsURL',
3219                 'deleteURL',
3220                 'importURL',
3221                 'insertURL',
3222                 'linkTarget',
3223                 'updateURL',
3224                 'shadeAlternatingRows',
3225                 'showBorders',
3226                 'showDeleteButton',
3227                 'showDetailsColumn',
3228                 'showExportButtons',
3229                 'showInsertNewButton',
3230                 'showPagination',
3231                 'showReports',
3232                 'showSurroundingBorder',
3233                 'showUpdateColumn',
3234                 'showViewPanel',
3235                 'timeout',
3236                 {name: 'disableAnalytics', prefix: true},
3237                 {name: 'maxRows', prefix: true, check: function(v) { return v > 0; }},
3238                 {name: 'showRows', prefix: true},
3239                 {name: 'offset', prefix: true, check: function(v) { return v !== 0; }},
3240                 {name: 'reportId', prefix: true},
3241                 {name: 'viewName', prefix: true}
3242             ]);
3243 
3244             // Sorts configured by the user when interacting with the grid. We need to pass these as URL parameters.
3245             if (LABKEY.Utils.isString(region._userSort) && region._userSort.length > 0) {
3246                 params[name + SORT_PREFIX] = region._userSort;
3247             }
3248 
3249             if (region.userFilters) {
3250                 $.each(region.userFilters, function(filterExp, filterValue) {
3251                     if (params[filterExp] == undefined) {
3252                         params[filterExp] = [];
3253                     }
3254                     params[filterExp].push(filterValue);
3255                 });
3256                 region.userFilters = {}; // they've been applied
3257             }
3258 
3259             // TODO: Get rid of this and incorporate it with the normal containerFilter checks
3260             if (region.userContainerFilter) {
3261                 params[name + CONTAINER_FILTER_NAME] = region.userContainerFilter;
3262             }
3263 
3264             if (region.parameters) {
3265                 var paramPrefix = name + PARAM_PREFIX;
3266                 $.each(region.parameters, function(parameter, value) {
3267                     var key = parameter;
3268                     if (parameter.indexOf(paramPrefix) !== 0) {
3269                         key = paramPrefix + parameter;
3270                     }
3271                     params[key] = value;
3272                 });
3273             }
3274         }
3275 
3276         //
3277         // apply all parameters
3278         //
3279 
3280         if (newParams) {
3281             $.each(newParams, function(i, pair) {
3282                 //
3283                 // Filters may repeat themselves #25337
3284                 //
3285                 if (_isFilter(region, pair[0])) {
3286                     if (params[pair[0]] == undefined) {
3287                         params[pair[0]] = [];
3288                     }
3289                     else if (!$.isArray(params[pair[0]])) {
3290                         params[pair[0]] = [params[pair[0]]];
3291                     }
3292                     params[pair[0]].push(pair[1]);
3293                 }
3294                 else {
3295                     params[pair[0]] = pair[1];
3296                 }
3297             });
3298         }
3299 
3300         //
3301         // Properties that cannot be modified
3302         //
3303 
3304         params.dataRegionName = region.name;
3305         params.schemaName = region.schemaName;
3306         params.viewName = region.viewName;
3307         params.reportId = region.reportId;
3308         params.returnURL = window.location.href;
3309         params['webpart.name'] = 'Query';
3310 
3311         if (region.queryName) {
3312             params.queryName = region.queryName;
3313         }
3314         else if (region.sql) {
3315             params.sql = region.sql;
3316         }
3317 
3318         var key = region.name + CONTAINER_FILTER_NAME;
3319         var cf = region.getContainerFilter.call(region);
3320         if (cf && !(key in params)) {
3321             params[key] = cf;
3322         }
3323 
3324         return params;
3325     };
3326 
3327     var _updateFilter = function(region, filter, skipPrefixes) {
3328         var params = [];
3329         if (filter) {
3330             params.push([filter.getURLParameterName(region.name), filter.getURLParameterValue()]);
3331         }
3332         _setParameters(region, params, [OFFSET_PREFIX].concat(skipPrefixes));
3333     };
3334 
3335     var _updateRequiresSelectionButtons = function(region, selectedCount) {
3336 
3337         // update the 'select all on page' checkbox state
3338         _getAllRowSelectors(region).each(function() {
3339             if (region.isPageSelected.call(region)) {
3340                 this.checked = true;
3341                 this.indeterminate = false;
3342             }
3343             else if (region.selectedCount > 0) {
3344                 // There are rows selected, but the are not visible on this page.
3345                 this.checked = false;
3346                 this.indeterminate = true;
3347             }
3348             else {
3349                 this.checked = false;
3350                 this.indeterminate = false;
3351             }
3352         });
3353 
3354         // If all rows have been selected (but not all rows are visible), show selection message
3355         if (region.totalRows && region.selectedCount == region.totalRows && !region.complete) {
3356             _showSelectMessage(region, 'All <span class="labkey-strong">' + region.totalRows + '</span> rows selected.');
3357         }
3358 
3359         // 10566: for javascript perf on IE stash the requires selection buttons
3360         if (!region._requiresSelectionButtons) {
3361             // escape ', ", and \
3362             var escaped = region.name.replace(/('|"|\\)/g, "\\$1");
3363             region._requiresSelectionButtons = $("a[labkey-requires-selection='" + escaped + "']");
3364         }
3365 
3366         region._requiresSelectionButtons.each(function() {
3367             var el = $(this);
3368 
3369             // handle min-count
3370             var minCount = el.attr('labkey-requires-selection-min-count');
3371             if (minCount) {
3372                 minCount = parseInt(minCount);
3373             }
3374             if (minCount === undefined) {
3375                 minCount = 1;
3376             }
3377 
3378             // handle max-count
3379             var maxCount = el.attr('labkey-requires-selection-max-count');
3380             if (maxCount) {
3381                 maxCount = parseInt(maxCount);
3382             }
3383 
3384             if (minCount <= selectedCount && (!maxCount || maxCount >= selectedCount)) {
3385                 el.addClass('labkey-button').removeClass('labkey-disabled-button');
3386             }
3387             else {
3388                 el.addClass('labkey-disabled-button').removeClass('labkey-button');
3389             }
3390         });
3391     };
3392 
3393     var HeaderLock = function(region) {
3394 
3395         var me = this,
3396             timeout;
3397 
3398         var calculateHeaderPosition = function(recalcPosition) {
3399             calculateLockPosition(recalcPosition);
3400             onScroll();
3401         };
3402 
3403         var calculateLockPosition = function(recalcPosition) {
3404             var el, s, src, i = 0;
3405 
3406             for (; i < me.rowContent.length; i++) {
3407                 src = $(me.firstRow[i]);
3408                 el = $(me.rowContent[i]);
3409 
3410                 s = {
3411                     width: src.width(),
3412                     height: el.height()
3413                 }; // note: width coming from data row not header
3414 
3415                 el.width(s.width); // 15420
3416 
3417                 $(me.rowSpacerContent[i]).height(s.height).width(s.width);
3418             }
3419 
3420             if (recalcPosition === true) {
3421                 me.hdrCoord = findPos();
3422             }
3423 
3424             me.hdrLocked = false;
3425         };
3426 
3427         var disable = function() {
3428             me.region._allowHeaderLock = false;
3429 
3430             if (timeout) {
3431                 clearTimeout(timeout);
3432             }
3433 
3434             $(window).unbind('load', onResize);
3435             $(window).unbind('resize', onResize);
3436             $(window).unbind('scroll', onScroll);
3437             $(document).unbind('DOMNodeInserted', onResize);
3438         };
3439 
3440         var ensurePaginationVisible = function() {
3441             if (me.paginationEl) {
3442                 // in case header locking is not on
3443                 if (!me.region.headerLock() || !me.hdrCoord || me.hdrCoord.length == 0) {
3444                     me.hdrCoord = findPos();
3445                 }
3446 
3447                 var measure = $('body').width() - me.hdrCoord[0];
3448                 if (measure < me.headerRow.width()) {
3449                     me.paginationEl.width(measure);
3450                 }
3451             }
3452         };
3453 
3454         /**
3455          * Returns an array of containing the following values:
3456          * [0] - X-coordinate of the top of the object relative to the offset parent. See Ext.Element.getXY()
3457          * [1] - Y-coordinate of the top of the object relative to the offset parent. See Ext.Element.getXY()
3458          * [2] - Y-coordinate of the bottom of the object.
3459          * [3] - The height of the header for this Data Region. This includes the button bar if it is present.
3460          * This method assumes interaction with the Header of the Data Region.
3461          */
3462         var findPos = function() {
3463             var o,
3464                 pos,
3465                 curbottom,
3466                 hdrOffset = 0;
3467 
3468             if (me.includeHeader) {
3469                 o = (me.hdrLocked ? me.headerSpacer : me.headerRow);
3470                 hdrOffset = me.headerSpacer.height();
3471             }
3472             else {
3473                 o = (me.hdrLocked ? me.colHeaderRowSpacer : me.colHeaderRow);
3474             }
3475 
3476             pos = o.offset();
3477             curbottom = pos.top + me.table.height() - (o.height() * 2);
3478 
3479             return [ pos.left, pos.top, curbottom, hdrOffset ];
3480         };
3481 
3482         var onResize = function() {
3483             if (!me.table) {
3484                 return;
3485             }
3486 
3487             if (me.region.headerLock()) {
3488                 if (timeout) {
3489                     clearTimeout(timeout);
3490                 }
3491                 timeout = setTimeout(resizeTask, 110);
3492             }
3493             else {
3494                 ensurePaginationVisible();
3495             }
3496         };
3497 
3498         /**
3499          * WARNING: This function is called often. Performance implications for each line.
3500          * NOTE: window.pageYOffset and pageXOffset are not available in IE7-. For these document.documentElement.scrollTop
3501          * and document.documentElement.scrollLeft could be used. Additionally, position: fixed is not recognized by
3502          * IE7- and can be best approximated with position: absolute and explicit top/left.
3503          */
3504         var onScroll = function() {
3505             var hrStyle, chrStyle;
3506 
3507             // calculate Y scrolling
3508             if (window.pageYOffset >= me.hdrCoord[1] && window.pageYOffset < me.hdrCoord[2]) {
3509                 // The header has reached the top of the window and needs to be locked
3510                 var tWidth = me.table.width();
3511 
3512                 hrStyle = {
3513                     top: 0,
3514                     position: 'fixed',
3515                     'min-width': tWidth,
3516                     'z-index': 9000 // 13229
3517                 };
3518                 chrStyle = {
3519                     top: me.hdrCoord[3],
3520                     position: 'fixed',
3521                     background: 'white',
3522                     'min-width': tWidth,
3523                     'box-shadow': '-2px 5px 5px #DCDCDC',
3524                     'z-index': 9000 // 13229
3525                 };
3526 
3527                 me.headerSpacer.css('display', 'table-row');
3528                 me.colHeaderRowSpacer.css('display', 'table-row');
3529                 me.headerRowContent.css('min-width', tWidth - 3);
3530                 me.hdrLocked = true;
3531             }
3532             else if (me.hdrLocked && window.pageYOffset >= me.hdrCoord[2]) {
3533                 // The bottom of the Data Region is near the top of the window and the locked header
3534                 // needs to start 'sliding' out of view.
3535                 var top = me.hdrCoord[2] - window.pageYOffset;
3536                 hrStyle = { top: top };
3537                 chrStyle = { top: (top + me.hdrCoord[3]) };
3538             }
3539             else if (me.hdrLocked) { // only reset if the header is locked
3540                 // the header should not be locked
3541                 reset(false);
3542             }
3543 
3544             // Calculate X Scrolling
3545             if (me.hdrLocked) {
3546                 if (!hrStyle) {
3547                     hrStyle = {};
3548                 }
3549                 if (!chrStyle) {
3550                     chrStyle = {};
3551                 }
3552 
3553                 hrStyle.left = me.hdrCoord[0] - window.pageXOffset;
3554                 chrStyle.left = me.hdrCoord[0] - window.pageXOffset;
3555             }
3556 
3557             if (hrStyle) {
3558                 me.headerRow.css(hrStyle);
3559             }
3560             if (chrStyle) {
3561                 me.colHeaderRow.css(chrStyle);
3562             }
3563         };
3564 
3565         /**
3566          * Adjusts the header styling to the best approximate of what the defaults are when the header is not locked
3567          */
3568         var reset = function(recalc) {
3569             me.hdrLocked = false;
3570             me.headerRow.css({
3571                 top: 'auto',
3572                 position: 'static',
3573                 'min-width': 0
3574             });
3575             me.headerRowContent.css('min-width', 0);
3576             me.colHeaderRow.css({
3577                 top: 'auto',
3578                 position: 'static',
3579                 'min-width': 0,
3580                 'box-shadow': 'none'
3581             });
3582             me.headerSpacer.hide();
3583             me.headerSpacer.height(me.headerRow.height());
3584             me.colHeaderRowSpacer.hide();
3585             calculateHeaderPosition(recalc);
3586         };
3587 
3588         var resizeTask = function() {
3589             reset(true);
3590             ensurePaginationVisible();
3591         };
3592 
3593         var validBrowser = function() {
3594             return true; // Ext.isIE9 || Ext.isIE10p || Ext.isWebKit || Ext.isGecko
3595         };
3596 
3597         // init
3598         if (!region.headerLock() || !validBrowser()) {
3599             region._allowHeaderLock = false;
3600             return;
3601         }
3602 
3603         this.region = region;
3604 
3605         // initialize constants
3606         this.headerRow = $('#' + region.domId + '-header-row');
3607         if (!this.headerRow) {
3608             region._allowHeaderLock = false;
3609             return;
3610         }
3611 
3612         this.table = $('#' + region.domId);
3613         this.headerRowContent = this.headerRow.children('td');
3614         this.headerSpacer = $('#' + region.domId + '-header-row-spacer');
3615         this.colHeaderRow = $('#' + region.domId + '-column-header-row');
3616         this.colHeaderRowSpacer = $('#' + region.domId + '-column-header-row-spacer');
3617         this.paginationEl = $('#' + region.domId + '-header');
3618 
3619         // check if the header row is being used
3620         this.includeHeader = this.headerRow.is(':visible'); // formerly isDisplayed()
3621 
3622         // initialize row contents
3623         // Check if we have colHeaderRow and colHeaderRowSpacer - they won't be present if there was an SQLException
3624         // during query execution, so we didn't get column metadata back
3625         if (this.colHeaderRow) {
3626             this.rowContent = this.colHeaderRow.find('td.labkey-column-header');
3627         }
3628         if (this.colHeaderRowSpacer) {
3629             this.rowSpacerContent = this.colHeaderRowSpacer.find('td.labkey-column-header');
3630         }
3631         this.firstRow = this.table.find('tr.labkey-alternate-row').first().children('td');
3632 
3633         // performance degradation
3634         var tooManyColumns = this.rowContent.length > 100;
3635         var tooManyRows = (region.rowCount && region.rowCount > 1000);
3636 
3637         if (tooManyColumns || tooManyRows) {
3638             region._allowHeaderLock = false;
3639             return;
3640         }
3641 
3642         // If no data rows exist just turn off header locking
3643         if (this.firstRow.length == 0) {
3644             this.firstRow = this.table.find('tr.labkey-row').first().children('td');
3645             if (this.firstRow.length == 0) {
3646                 region._allowHeaderLock = false;
3647                 return;
3648             }
3649         }
3650 
3651         // initialize additional listeners
3652         $(window).one('load', onResize);
3653         $(window).on('resize', onResize);
3654         $(document).bind('DOMNodeInserted', onResize); // Issue #13121
3655         $(window).scroll(onScroll);
3656 
3657         ensurePaginationVisible();
3658 
3659         // initialize panel listeners
3660         // 13669: customize view jumping when using drag/drop to reorder columns/filters/sorts
3661         // must manage DOMNodeInserted Listeners due to panels possibly dynamically adding elements to page
3662         region.on('afterpanelshow', function() {
3663             $(document).unbind('DOMNodeInserted', onResize); // suspend listener
3664             onResize();
3665         }, this);
3666 
3667         region.on('afterpanelhide', function() {
3668             $(document).bind('DOMNodeInserted', onResize); // resume listener
3669             onResize();
3670         }, this);
3671 
3672         this.hdrCoord = [];
3673 
3674         reset(true);
3675 
3676         // public methods
3677         return {
3678             disable: disable
3679         };
3680     };
3681 
3682     //
3683     // LOADER
3684     //
3685     LABKEY.DataRegion.create = function(config) {
3686 
3687         var region = LABKEY.DataRegions[config.name];
3688 
3689         if (region) {
3690             // region already exists, update properties
3691             $.each(config, function(key, value) {
3692                 region[key] = value;
3693             });
3694             if (!config.view) {
3695                 // when switching back to 'default' view, needs to clear region.view
3696                 region.view = undefined;
3697             }
3698             region._init(config);
3699         }
3700         else {
3701             // instantiate a new region
3702             region = new LABKEY.DataRegion(config);
3703             LABKEY.DataRegions[region.name] = region;
3704         }
3705 
3706         return region;
3707     };
3708 
3709     LABKEY.DataRegion.loadViewDesigner = function(cb, scope) {
3710         LABKEY.requiresExt4Sandbox(function() {
3711             LABKEY.requiresScript('internal/ViewDesigner', cb, scope);
3712         });
3713     };
3714 
3715     LABKEY.DataRegion.getCustomViewEditableErrors = function(customView) {
3716         var errors = [];
3717         if (customView && !customView.editable) {
3718             errors.push("The view is read-only and cannot be edited.");
3719         }
3720         return errors;
3721     };
3722 
3723     LABKEY.DataRegion.registerPane = function(regionName, callback, scope) {
3724         var region = LABKEY.DataRegions[regionName];
3725         if (region) {
3726             callback.call(scope || region, region);
3727             return;
3728         }
3729         else if (!_paneCache[regionName]) {
3730             _paneCache[regionName] = [];
3731         }
3732 
3733         _paneCache[regionName].push({cb: callback, scope: scope});
3734     };
3735 
3736     LABKEY.DataRegion.selectAll = function(config) {
3737         var params = {};
3738         if (!config.url) {
3739             // DataRegion doesn't have selectAllURL so generate url and query parameters manually
3740             config.url = LABKEY.ActionURL.buildURL('query', 'selectAll.api', config.containerPath);
3741 
3742             config.dataRegionName = config.dataRegionName || 'query';
3743 
3744             params = LABKEY.Query.buildQueryParams(
3745                     config.schemaName,
3746                     config.queryName,
3747                     config.filters,
3748                     null,
3749                     config.dataRegionName
3750             );
3751 
3752             if (config.viewName)
3753                 params[config.dataRegionName + VIEWNAME_PREFIX] = config.viewName;
3754 
3755             if (config.containerFilter)
3756                 params.containerFilter = config.containerFilter;
3757 
3758             if (config.selectionKey)
3759                 params[config.dataRegionName + '.selectionKey'] = config.selectionKey;
3760 
3761             $.each(config.parameters, function(propName, value) {
3762                 params[config.dataRegionName + PARAM_PREFIX + propName] = value;
3763             });
3764 
3765             if (config.ignoreFilter) {
3766                 params[config.dataRegionName + '.ignoreFilter'] = true;
3767             }
3768 
3769             // NOTE: ignore maxRows, showRows, and offset
3770         }
3771 
3772         LABKEY.Ajax.request({
3773             url: config.url,
3774             method: 'POST',
3775             params: params,
3776             success: LABKEY.Utils.getCallbackWrapper(LABKEY.Utils.getOnSuccess(config), config.scope),
3777             failure: LABKEY.Utils.getCallbackWrapper(LABKEY.Utils.getOnFailure(config), config.scope, true)
3778         });
3779     };
3780 
3781     /**
3782      * Static method to add or remove items from the selection for a given {@link #selectionKey}.
3783      *
3784      * @param config A configuration object with the following properties:
3785      * @param {String} config.selectionKey See {@link #selectionKey}.
3786      * @param {Array} config.ids Array of primary key ids for each row to select/unselect.
3787      * @param {Boolean} config.checked If true, the ids will be selected, otherwise unselected.
3788      * @param {Function} config.success The function to be called upon success of the request.
3789      * The callback will be passed the following parameters:
3790      * <ul>
3791      * <li><b>data:</b> an object with the property 'count' to indicate the updated selection count.
3792      * <li><b>response:</b> The XMLHttpResponse object</li>
3793      * </ul>
3794      * @param {Function} [config.failure] The function to call upon error of the request.
3795      * The callback will be passed the following parameters:
3796      * <ul>
3797      * <li><b>errorInfo:</b> an object containing detailed error information (may be null)</li>
3798      * <li><b>response:</b> The XMLHttpResponse object</li>
3799      * </ul>
3800      * @param {Object} [config.scope] An optional scoping object for the success and error callback functions (default to this).
3801      * @param {string} [config.containerPath] An alternate container path. If not specified, the current container path will be used.
3802      *
3803      * @see LABKEY.DataRegion#getSelected
3804      * @see LABKEY.DataRegion#clearSelected
3805      */
3806     LABKEY.DataRegion.setSelected = function(config) {
3807         // Formerly LABKEY.DataRegion.setSelected
3808         var url = LABKEY.ActionURL.buildURL("query", "setSelected.api", config.containerPath,
3809                 { 'key': config.selectionKey, 'checked': config.checked });
3810 
3811         LABKEY.Ajax.request({
3812             url: url,
3813             method: "POST",
3814             params: { id: config.ids || config.id },
3815             scope: config.scope,
3816             success: LABKEY.Utils.getCallbackWrapper(LABKEY.Utils.getOnSuccess(config), config.scope),
3817             failure: LABKEY.Utils.getCallbackWrapper(LABKEY.Utils.getOnFailure(config), config.scope, true)
3818         });
3819     };
3820 
3821     /**
3822      * Static method to clear all selected items for a given {@link #selectionKey}.
3823      *
3824      * @param config A configuration object with the following properties:
3825      * @param {String} config.selectionKey See {@link #selectionKey}.
3826      * @param {Function} config.success The function to be called upon success of the request.
3827      * The callback will be passed the following parameters:
3828      * <ul>
3829      * <li><b>data:</b> an object with the property 'count' of 0 to indicate an empty selection.
3830      * <li><b>response:</b> The XMLHttpResponse object</li>
3831      * </ul>
3832      * @param {Function} [config.failure] The function to call upon error of the request.
3833      * The callback will be passed the following parameters:
3834      * <ul>
3835      * <li><b>errorInfo:</b> an object containing detailed error information (may be null)</li>
3836      * <li><b>response:</b> The XMLHttpResponse object</li>
3837      * </ul>
3838      * @param {Object} [config.scope] An optional scoping object for the success and error callback functions (default to this).
3839      * @param {string} [config.containerPath] An alternate container path. If not specified, the current container path will be used.
3840      *
3841      * @see LABKEY.DataRegion#setSelected
3842      * @see LABKEY.DataRegion#getSelected
3843      */
3844     LABKEY.DataRegion.clearSelected = function(config) {
3845         var url = LABKEY.ActionURL.buildURL('query', 'clearSelected.api', config.containerPath,
3846                 { 'key': config.selectionKey });
3847 
3848         LABKEY.Ajax.request({ url: url });
3849     };
3850 
3851     /**
3852      * Static method to get all selected items for a given {@link #selectionKey}.
3853      *
3854      * @param config A configuration object with the following properties:
3855      * @param {String} config.selectionKey See {@link #selectionKey}.
3856      * @param {Function} config.success The function to be called upon success of the request.
3857      * The callback will be passed the following parameters:
3858      * <ul>
3859      * <li><b>data:</b> an object with the property 'selected' that is an array of the primary keys for the selected rows.
3860      * <li><b>response:</b> The XMLHttpResponse object</li>
3861      * </ul>
3862      * @param {Function} [config.failure] The function to call upon error of the request.
3863      * The callback will be passed the following parameters:
3864      * <ul>
3865      * <li><b>errorInfo:</b> an object containing detailed error information (may be null)</li>
3866      * <li><b>response:</b> The XMLHttpResponse object</li>
3867      * </ul>
3868      * @param {Object} [config.scope] An optional scoping object for the success and error callback functions (default to this).
3869      * @param {string} [config.containerPath] An alternate container path. If not specified, the current container path will be used.
3870      *
3871      * @see LABKEY.DataRegion#setSelected
3872      * @see LABKEY.DataRegion#clearSelected
3873      */
3874     LABKEY.DataRegion.getSelected = function(config) {
3875         var url = LABKEY.ActionURL.buildURL('query', 'getSelected.api', config.containerPath,
3876                 { 'key': config.selectionKey });
3877 
3878         LABKEY.Ajax.request({
3879             url: url,
3880             success: LABKEY.Utils.getCallbackWrapper(LABKEY.Utils.getOnSuccess(config), config.scope),
3881             failure: LABKEY.Utils.getCallbackWrapper(LABKEY.Utils.getOnFailure(config), config.scope, true)
3882         });
3883     };
3884 
3885     /**
3886      * MessageArea wraps the display of messages in a DataRegion.
3887      * @param dataRegion - The dataregion that the MessageArea will bind itself to.
3888      * @param {String[]} [messages] - An initial messages object containing mappings of 'part' to 'msg'
3889      * @constructor
3890      */
3891     var MessageArea = function(dataRegion, messages) {
3892         this.bindRegion(dataRegion);
3893 
3894         if (messages) {
3895             this.setMessages(messages);
3896         }
3897     };
3898 
3899     var MsgProto = MessageArea.prototype;
3900 
3901     MsgProto.bindRegion = function(region) {
3902         this.parentDataRegion = region;
3903         this.parentSel = '#' + region.domId + '-msgbox';
3904     };
3905 
3906     MsgProto.toJSON = function() {
3907         return this.parts;
3908     };
3909 
3910     MsgProto.addMessage = function(msg, part, append) {
3911         part = part || 'info';
3912 
3913         var p = part.toLowerCase();
3914         if (append && this.parts.hasOwnProperty(p))
3915         {
3916             this.parts[p] += msg;
3917             this.render(p, msg);
3918         }
3919         else
3920         {
3921             this.parts[p] = msg;
3922             this.render(p);
3923         }
3924 
3925         this.expand();
3926     };
3927 
3928     MsgProto.getMessage = function(part) {
3929         return this.parts[part.toLowerCase()];
3930     };
3931 
3932     MsgProto.hasMessage = function(part) {
3933         return this.getMessage(part) !== undefined;
3934     };
3935 
3936     MsgProto.hasContent = function() {
3937         return this.parts && Object.keys(this.parts).length > 0;
3938     };
3939 
3940     MsgProto.removeAll = function() {
3941         this.parts = {};
3942         this.render();
3943     };
3944 
3945     MsgProto.removeMessage = function(part) {
3946         var p = part.toLowerCase();
3947         if (this.parts.hasOwnProperty(p)) {
3948             this.parts[p] = undefined;
3949             this.render();
3950         }
3951     };
3952 
3953     MsgProto.setMessages = function(messages) {
3954         if (LABKEY.Utils.isObject(messages)) {
3955             this.parts = messages;
3956         }
3957         else {
3958             this.parts = {};
3959         }
3960     };
3961 
3962     MsgProto.getParent = function() {
3963         var parent = $(this.parentSel);
3964 
3965         // ensure container div is present
3966         if (parent.find('div.dataregion_msgbox_ct').length == 0) {
3967             parent.find('td.labkey-dataregion-msgbox').append('<div class="dataregion_msgbox_ct"></div>');
3968         }
3969 
3970         return parent;
3971     };
3972 
3973     MsgProto.render = function(partToUpdate, appendMsg) {
3974         var parentCt = this.getParent().find('.dataregion_msgbox_ct'),
3975             hasMsg = false, msgCls = '',
3976             partCls, partEl,
3977             me = this;
3978 
3979         $.each(this.parts, function(part, msg) {
3980             partCls = 'labkey-dataregion-msg-part-' + part;
3981 
3982             if (msg) {
3983 
3984                 partEl = parentCt.find('.' + partCls);
3985                 if (partEl.length == 0) {
3986 
3987                     msgCls = 'labkey-dataregion-msg ' + partCls + (hasMsg ? ' labkey-dataregion-msg-sep' : '');
3988                     parentCt.append('<div class="' + msgCls + '">' + msg + '</div>');
3989                 }
3990                 else if (partToUpdate != undefined && partToUpdate == part) {
3991 
3992                     if (appendMsg != undefined)
3993                         partEl.append(appendMsg);
3994                     else
3995                         partEl.html(msg)
3996                 }
3997 
3998                 hasMsg = true;
3999             }
4000             else {
4001                 parentCt.find('.' + partCls).remove();
4002                 delete me.parts[part];
4003             }
4004         });
4005 
4006         if (hasMsg) {
4007             this.show();
4008             $(this).trigger('rendermsg', [this, this.parts]);
4009         }
4010         else {
4011             this.hide();
4012             parentCt.html('');
4013         }
4014     };
4015 
4016     MsgProto.expand = function() {
4017         if (this.isVisible()) {
4018             this.getParent().find('.labkey-dataregion-msg').show();
4019 
4020             var toggle = this.getToggleEl();
4021             toggle.removeClass('fa-plus');
4022             toggle.addClass('fa-minus');
4023             toggle.prop('title', 'Collapse message');
4024 
4025             _getHeaderSelector(this.parentDataRegion).trigger('resize');
4026         }
4027     };
4028 
4029     MsgProto.collapse = function() {
4030         if (this.isVisible()) {
4031             this.getParent().find('.labkey-dataregion-msg').hide();
4032 
4033             var toggle = this.getToggleEl();
4034             toggle.removeClass('fa-minus');
4035             toggle.addClass('fa-plus');
4036             toggle.prop('title', 'Expand message');
4037 
4038             _getHeaderSelector(this.parentDataRegion).trigger('resize');
4039         }
4040     };
4041 
4042     MsgProto.getToggleEl = function() {
4043         return this.getParent().find('.labkey-dataregion-msg-toggle');
4044     };
4045 
4046     MsgProto.show = function() { this.getParent().show(); };
4047     MsgProto.hide = function() { this.getParent().hide(); };
4048     MsgProto.isVisible = function() { return $(this.parentSel + ':visible').length > 0; };
4049     MsgProto.find = function(selector) {
4050         return this.getParent().find('.dataregion_msgbox_ct').find(selector);
4051     };
4052     MsgProto.on = function(evt, callback, scope) { $(this).bind(evt, $.proxy(callback, scope)); };
4053 
4054     /**
4055      * @description Constructs a LABKEY.QueryWebPart class instance
4056      * @class The LABKEY.QueryWebPart simplifies the task of dynamically adding a query web part to your page.  Please use
4057      * this class for adding query web parts to a page instead of {@link LABKEY.WebPart},
4058      * which can be used for other types of web parts.
4059      *              <p>Additional Documentation:
4060      *              <ul>
4061      *                  <li><a href= "https://www.labkey.org/wiki/home/Documentation/page.view?name=webPartConfig">
4062      *  				        Web Part Configuration Properties</a></li>
4063      *                  <li><a href="https://www.labkey.org/wiki/home/Documentation/page.view?name=findNames">
4064      *                      How To Find schemaName, queryName & viewName</a></li>
4065      *                  <li><a href="https://www.labkey.org/wiki/home/Documentation/page.view?name=javascriptTutorial">LabKey JavaScript API Tutorial</a> and
4066      *                      <a href="https://www.labkey.org/wiki/home/Study/demo/page.view?name=reagentRequest">Demo</a></li>
4067      *                  <li><a href="https://www.labkey.org/wiki/home/Documentation/page.view?name=labkeySql">
4068      *                      LabKey SQL Reference</a></li>
4069      *              </ul>
4070      *           </p>
4071      * @constructor
4072      * @param {Object} config A configuration object with the following possible properties:
4073      * @param {String} config.schemaName The name of the schema the web part will query.
4074      * @param {String} config.queryName The name of the query within the schema the web part will select and display.
4075      * @param {String} [config.viewName] the name of a saved view you wish to display for the given schema and query name.
4076      * @param {String} [config.reportId] the report id of a saved report you wish to display for the given schema and query name.
4077      * @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>.
4078      * If not supplied in the configuration, you must call the render() method to render the part into the page.
4079      * @param {String} [config.errorType] A parameter to specify how query parse errors are returned. (default 'html'). Valid
4080      * values are either 'html' or 'json'. If 'html' is specified the error will be rendered to an HTML view, if 'json' is specified
4081      * the errors will be returned to the callback handlers as an array of objects named 'parseErrors' with the following properties:
4082      * <ul>
4083      *  <li><b>msg</b>: The error message.</li>
4084      *  <li><b>line</b>: The line number the error occurred at (optional).</li>
4085      *  <li><b>col</b>: The column number the error occurred at (optional).</li>
4086      *  <li><b>errorStr</b>: The line from the source query that caused the error (optional).</li>
4087      * </ul>
4088      * @param {String} [config.sql] A SQL query that can be used instead of an existing schema name/query name combination.
4089      * @param {Object} [config.metadata] Metadata that can be applied to the properties of the table fields. Currently, this option is only
4090      * available if the query has been specified through the config.sql option. For full documentation on
4091      * 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>.
4092      * This object may contain the following properties:
4093      * <ul>
4094      *  <li><b>type</b>: The type of metadata being specified. Currently, only 'xml' is supported.</li>
4095      *  <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>
4096      * </ul>
4097      * @param {String} [config.title] A title for the web part. If not supplied, the query name will be used as the title.
4098      * @param {String} [config.titleHref] If supplied, the title will be rendered as a hyperlink with this value as the href attribute.
4099      * @param {String} [config.buttonBarPosition] DEPRECATED--see config.buttonBar.position
4100      * @param {boolean} [config.allowChooseQuery] If the button bar is showing, whether or not it should be include a button
4101      * to let the user choose a different query.
4102      * @param {boolean} [config.allowChooseView] If the button bar is showing, whether or not it should be include a button
4103      * to let the user choose a different view.
4104      * @param {String} [config.detailsURL] Specify or override the default details URL for the table with one of the form
4105      * "/controller/action.view?id=${RowId}" or "org.labkey.package.MyController$ActionAction.class?id=${RowId}"
4106      * @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.
4107      * @param {String} [config.updateURL] Specify or override the default updateURL for the table with one of the form
4108      * "/controller/action.view?id=${RowId}" or "org.labkey.package.MyController$ActionAction.class?id=${RowId}"
4109      * @param {boolean} [config.showUpdateColumn] If the underlying table has an update URL, show a column that renders an [edit] link (default true).
4110      * @param {String} [config.insertURL] Specify or override the default insert URL for the table with one of the form
4111      * "/controller/insertAction.view" or "org.labkey.package.MyController$InsertActionAction.class"
4112      * @param {String} [config.importURL] Specify or override the default bulk import URL for the table with one of the form
4113      * "/controller/importAction.view" or "org.labkey.package.MyController$ImportActionAction.class"
4114      * @param {String} [config.deleteURL] Specify or override the default delete URL for the table with one of the form
4115      * "/controller/action.view" or "org.labkey.package.MyController$ActionAction.class". The keys for the selected rows
4116      * will be included in the POST.
4117      * @param {boolean} [config.showInsertNewButton] If the underlying table has an insert URL, show an "Insert New" button in the button bar (default true).
4118      * @param {boolean} [config.showDeleteButton] Show a "Delete" button in the button bar (default true).
4119      * @param {boolean} [config.showReports] If true, show reports on the Views menu (default true).
4120      * @param {boolean} [config.showExportButtons] Show the export button menu in the button bar (default true).
4121      * @param {boolean} [config.showBorders] Render the table with borders (default true).
4122      * @param {boolean} [config.showSurroundingBorder] Render the table with a surrounding border (default true).
4123      * @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).
4124      *  If 'showDeleteButton' is true, the checkboxes will be  included regardless of the 'showRecordSelectors' config option.
4125      * @param {boolean} [config.showPagination] Show the pagination links and count (default true).
4126      * @param {boolean} [config.shadeAlternatingRows] Shade every other row with a light gray background color (default true).
4127      * @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.
4128      * @param {Object} [config.buttonBar] Button bar configuration. This object may contain any of the following properties:
4129      * <ul>
4130      *  <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>
4131      *  <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>
4132      *  <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.
4133      *                  to reference standard buttons, use one of the properties on {@link #standardButtons}, or simply include a string
4134      *                  that matches the button's caption. To include a new button configuration, create an object with the following properties:
4135      *      <ul>
4136      *          <li><b>text</b>: The text you want displayed on the button (aka the caption).</li>
4137      *          <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.
4138      *                          Specify this or a handler function, but not both.</li>
4139      *          <li><b>handler</b>: A reference to the JavaScript function you want called when the button is clicked.</li>
4140      *          <li><b>permission</b>: Optional. Permission that the current user must possess to see the button.
4141      *                          Valid options are 'READ', 'INSERT', 'UPDATE', 'DELETE', and 'ADMIN'.
4142      *                          Default is 'READ' if permissionClass is not specified.</li>
4143      *          <li><b>permissionClass</b>: Optional. If permission (see above) is not specified, the fully qualified Java class
4144      *                           name of the permission that the user must possess to view the button.</li>
4145      *          <li><b>requiresSelection</b>: A boolean value (true/false) indicating whether the button should only be enabled when
4146      *                          data rows are checked/selected.</li>
4147      *          <li><b>items</b>: To create a drop-down menu button, set this to an array of menu item configurations.
4148      *                          Each menu item configuration can specify any of the following properties:
4149      *              <ul>
4150      *                  <li><b>text</b>: The text of the menu item.</li>
4151      *                  <li><b>handler</b>: A reference to the JavaScript function you want called when the menu item is clicked.</li>
4152      *                  <li><b>icon</b>: A url to an image to use as the menu item's icon.</li>
4153      *                  <li><b>items</b>: An array of sub-menu item configurations. Used for fly-out menus.</li>
4154      *              </ul>
4155      *          </li>
4156      *      </ul>
4157      *  </li>
4158      * </ul>
4159      * @param {String} [config.sort] A base sort order to use. This is a comma-separated list of column names, each of
4160      * which may have a - prefix to indicate a descending sort. It will be treated as the final sort, after any that the user
4161      * has defined in a custom view or through interacting with the grid column headers.
4162      * @param {String} [config.removeableSort] An additional sort order to use. This is a comma-separated list of column names, each of
4163      * which may have a - prefix to indicate a descending sort. It will be treated as the first sort, before any that the user
4164      * has defined in a custom view or through interacting with the grid column headers.
4165      * @param {Array} [config.filters] A base set of filters to apply. This should be an array of {@link LABKEY.Filter} objects
4166      * each of which is created using the {@link LABKEY.Filter.create} method. These filters cannot be removed by the user
4167      * interacting with the UI.
4168      * For compatibility with the {@link LABKEY.Query} object, you may also specify base filters using config.filterArray.
4169      * @param {Array} [config.removeableFilters] A set of filters to apply. This should be an array of {@link LABKEY.Filter} objects
4170      * each of which is created using the {@link LABKEY.Filter.create} method. These filters can be modified or removed by the user
4171      * interacting with the UI.
4172      * @param {Object} [config.parameters] Map of name (string)/value pairs for the values of parameters if the SQL
4173      * references underlying queries that are parameterized. For example, the following passes two parameters to the query: {'Gender': 'M', 'CD4': '400'}.
4174      * 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
4175      * <a href="https://www.labkey.org/wiki/home/Documentation/page.view?name=paramsql">Parameterized SQL Queries</a>.
4176      * @param {Array} [config.aggregates] An array of aggregate definitions. The objects in this array should have the properties:
4177      * <ul>
4178      *     <li><b>column:</b> The name of the column to be aggregated.</li>
4179      *     <li><b>type:</b> The aggregate type (see {@link LABKEY.AggregateTypes})</li>
4180      *     <li><b>label:</b> Optional label used when rendering the aggregate row.
4181      * </ul>
4182      * @param {String} [config.showRows] Either 'paginated' (the default) 'selected', 'unselected', 'all', or 'none'.
4183      *        When 'paginated', the maxRows and offset parameters can be used to page through the query's result set rows.
4184      *        When 'selected' or 'unselected' the set of rows selected or unselected by the user in the grid view will be returned.
4185      *        You can programmatically get and set the selection using the {@link LABKEY.DataRegion.setSelected} APIs.
4186      *        Setting <code>config.maxRows</code> to -1 is the same as 'all'
4187      *        and setting <code>config.maxRows</code> to 0 is the same as 'none'.
4188      * @param {Integer} [config.maxRows] The maximum number of rows to return from the server (defaults to 100).
4189      *        If you want to return all possible rows, set this config property to -1.
4190      * @param {Integer} [config.offset] The index of the first row to return from the server (defaults to 0).
4191      *        Use this along with the maxRows config property to request pages of data.
4192      * @param {String} [config.dataRegionName] The name to be used for the data region. This should be unique within
4193      * the set of query views on the page. If not supplied, a unique name is generated for you.
4194      * @param {String} [config.linkTarget] The name of a browser window/tab in which to open URLs rendered in the
4195      * QueryWebPart. If not supplied, links will generally be opened in the same browser window/tab where the QueryWebPart.
4196      * @param {String} [config.frame] The frame style to use for the web part. This may be one of the following:
4197      * 'div', 'portal', 'none', 'dialog', 'title', 'left-nav'.
4198      * @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".
4199      * @param {String} [config.bodyClass] A CSS style class that will be added to the enclosing element for the web part.
4200      * @param {Function} [config.success] A function to call after the part has been rendered. It will be passed two arguments:
4201      * <ul>
4202      * <li><b>dataRegion:</b> the LABKEY.DataRegion object representing the rendered QueryWebPart</li>
4203      * <li><b>request:</b> the XMLHTTPRequest that was issued to the server</li>
4204      * </ul>
4205      * @param {Function} [config.failure] A function to call if the request to retrieve the content fails. It will be passed three arguments:
4206      * <ul>
4207      * <li><b>json:</b> JSON object containing the exception.</li>
4208      * <li><b>response:</b> The XMLHttpRequest object containing the response data.</li>
4209      * <li><b>options:</b> The parameter to the request call.</li>
4210      * </ul>
4211      * @param {Object} [config.scope] An object to use as the callback function's scope. Defaults to this.
4212      * @param {int} [config.timeout] A timeout for the AJAX call, in milliseconds. Default is 30000 (30 seconds).
4213      * @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.
4214      * @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.
4215      * @example
4216      * <div id='queryTestDiv1'/>
4217      * <script type="text/javascript">
4218      var qwp1 = new LABKEY.QueryWebPart({
4219 
4220              renderTo: 'queryTestDiv1',
4221              title: 'My Query Web Part',
4222              schemaName: 'lists',
4223              queryName: 'People',
4224              buttonBarPosition: 'none',
4225              aggregates: [
4226                     {column: 'First', type: LABKEY.AggregateTypes.COUNT, label: 'Total People'},
4227                     {column: 'Age', type: LABKEY.AggregateTypes.MEAN}
4228              ],
4229              filters: [
4230                     LABKEY.Filter.create('Last', 'Flintstone')
4231              ],
4232                     sort: '-Last'
4233              });
4234 
4235              //note that you may also register for the 'render' event
4236              //instead of using the success config property.
4237              //registering for events is done using Ext event registration.
4238              //Example:
4239              qwp1.on("render", onRender);
4240              function onRender()
4241              {
4242                 //...do something after the part has rendered...
4243              }
4244 
4245              ///////////////////////////////////////
4246              // Custom Button Bar Example
4247 
4248              var qwp1 = new LABKEY.QueryWebPart({
4249              renderTo: 'queryTestDiv1',
4250              title: 'My Query Web Part',
4251              schemaName: 'lists',
4252              queryName: 'People',
4253              buttonBar: {
4254                     includeStandardButtons: true,
4255                     items:[
4256                         LABKEY.QueryWebPart.standardButtons.views,
4257                         {text: 'Test', url: LABKEY.ActionURL.buildURL('project', 'begin')},
4258                         {text: 'Test Script', onClick: "alert('Hello World!'); return false;"},
4259                         {text: 'Test Handler', handler: onTestHandler},
4260                         {text: 'Test Menu', items: [
4261                         {text: 'Item 1', handler: onItem1Handler},
4262                         {text: 'Fly Out', items: [
4263                             {text: 'Sub Item 1', handler: onItem1Handler}
4264                             ]},
4265                             '-', //separator
4266                             {text: 'Item 2', handler: onItem2Handler}
4267                         ]},
4268                         LABKEY.QueryWebPart.standardButtons.exportRows
4269                     ]}
4270              });
4271 
4272              function onTestHandler(dataRegion)
4273              {
4274                  alert("onTestHandler called!");
4275                  return false;
4276              }
4277 
4278              function onItem1Handler(dataRegion)
4279              {
4280                  alert("onItem1Handler called!");
4281              }
4282 
4283              function onItem2Handler(dataRegion)
4284              {
4285                  alert("onItem2Handler called!");
4286              }
4287 
4288              </script>
4289      */
4290     LABKEY.QueryWebPart = function(config) {
4291         config._useQWPDefaults = true;
4292         return LABKEY.DataRegion.create(config);
4293     };
4294 
4295     if (!$.fn.tab) {
4296         // TAB CLASS DEFINITION
4297         // ====================
4298         var Tab = function (element) {
4299             this.element = $(element);
4300         };
4301 
4302         Tab.VERSION = '3.3.6';
4303 
4304         Tab.TRANSITION_DURATION = 150;
4305 
4306         Tab.prototype.show = function () {
4307             var $this    = this.element;
4308             var $ul      = $this.closest('ul:not(.dropdown-menu)');
4309             var selector = $this.data('target');
4310 
4311             if (!selector) {
4312                 selector = $this.attr('href');
4313                 selector = selector && selector.replace(/.*(?=#[^\s]*$)/, ''); // strip for ie7
4314             }
4315 
4316             if ($this.parent('li').hasClass('active')) {
4317                 return;
4318             }
4319 
4320             var $previous = $ul.find('.active:last a');
4321             var hideEvent = $.Event('hide.bs.tab', {
4322                 relatedTarget: $this[0]
4323             });
4324             var showEvent = $.Event('show.bs.tab', {
4325                 relatedTarget: $previous[0]
4326             });
4327 
4328             $previous.trigger(hideEvent);
4329             $this.trigger(showEvent);
4330 
4331             if (showEvent.isDefaultPrevented() || hideEvent.isDefaultPrevented()) {
4332                 return;
4333             }
4334 
4335             var $target = $(selector);
4336 
4337             this.activate($this.closest('li'), $ul);
4338             this.activate($target, $target.parent(), function () {
4339                 $previous.trigger({
4340                     type: 'hidden.bs.tab',
4341                     relatedTarget: $this[0]
4342                 });
4343                 $this.trigger({
4344                     type: 'shown.bs.tab',
4345                     relatedTarget: $previous[0]
4346                 })
4347             })
4348         };
4349 
4350         Tab.prototype.activate = function (element, container, callback) {
4351             var $active = container.find('> .active');
4352             var transition = callback
4353                     && $.support.transition
4354                     && ($active.length && $active.hasClass('fade') || !!container.find('> .fade').length);
4355 
4356             function next() {
4357                 $active
4358                         .removeClass('active')
4359                         .find('> .dropdown-menu > .active')
4360                         .removeClass('active')
4361                         .end()
4362                         .find('[data-toggle="tab"]')
4363                         .attr('aria-expanded', false);
4364 
4365                 element
4366                         .addClass('active')
4367                         .find('[data-toggle="tab"]')
4368                         .attr('aria-expanded', true);
4369 
4370                 if (transition) {
4371                     element[0].offsetWidth; // reflow for transition
4372                     element.addClass('in');
4373                 }
4374                 else {
4375                     element.removeClass('fade');
4376                 }
4377 
4378                 if (element.parent('.dropdown-menu').length) {
4379                     element
4380                             .closest('li.dropdown')
4381                             .addClass('active')
4382                             .end()
4383                             .find('[data-toggle="tab"]')
4384                             .attr('aria-expanded', true)
4385                 }
4386 
4387                 callback && callback();
4388             }
4389 
4390             $active.length && transition ?
4391                     $active.one('bsTransitionEnd', next).emulateTransitionEnd(Tab.TRANSITION_DURATION) :
4392                     next();
4393 
4394             $active.removeClass('in');
4395         };
4396 
4397 
4398         // TAB PLUGIN DEFINITION
4399         // =====================
4400 
4401         function Plugin(option) {
4402             return this.each(function () {
4403                 var $this = $(this);
4404                 var data  = $this.data('bs.tab');
4405 
4406                 if (!data) $this.data('bs.tab', (data = new Tab(this)));
4407                 if (typeof option == 'string') data[option]()
4408             })
4409         }
4410 
4411         var old = $.fn.tab;
4412 
4413         $.fn.tab = Plugin;
4414         $.fn.tab.Constructor = Tab;
4415 
4416 
4417         // TAB NO CONFLICT
4418         // ===============
4419 
4420         $.fn.tab.noConflict = function () {
4421             $.fn.tab = old;
4422             return this;
4423         };
4424 
4425 
4426         // TAB DATA-API
4427         // ============
4428 
4429         var clickHandler = function (e) {
4430             e.preventDefault();
4431             Plugin.call($(this), 'show');
4432         };
4433 
4434         $(document)
4435                 .on('click.bs.tab.data-api', '[data-toggle="tab"]', clickHandler)
4436                 .on('click.bs.tab.data-api', '[data-toggle="pill"]', clickHandler)
4437     }
4438 
4439 })(jQuery);
4440 
4441 /**
4442  * A read-only object that exposes properties representing standard buttons shown in LabKey data grids.
4443  * These are used in conjunction with the buttonBar configuration. The following buttons are currently defined:
4444  * <ul>
4445  *  <li>LABKEY.QueryWebPart.standardButtons.query</li>
4446  *  <li>LABKEY.QueryWebPart.standardButtons.views</li>
4447  *  <li>LABKEY.QueryWebPart.standardButtons.insertNew</li>
4448  *  <li>LABKEY.QueryWebPart.standardButtons.deleteRows</li>
4449  *  <li>LABKEY.QueryWebPart.standardButtons.exportRows</li>
4450  *  <li>LABKEY.QueryWebPart.standardButtons.print</li>
4451  *  <li>LABKEY.QueryWebPart.standardButtons.pageSize</li>
4452  * </ul>
4453  * @name standardButtons
4454  * @memberOf LABKEY.QueryWebPart#
4455  */
4456 LABKEY.QueryWebPart.standardButtons = {
4457     query: 'query',
4458     views: 'grid views',
4459     insertNew: 'insert new',
4460     deleteRows: 'delete',
4461     exportRows: 'export',
4462     print: 'print',
4463     pageSize: 'paging'
4464 };
4465 
4466 /**
4467  * Requests the query web part content and renders it within the element identified by the renderTo parameter.
4468  * Note that you do not need to call this method explicitly if you specify a renderTo property on the config object
4469  * handed to the class constructor. If you do not specify renderTo in the config, then you must call this method
4470  * passing the id of the element in which you want the part rendered
4471  * @function
4472  * @param renderTo The id of the element in which you want the part rendered.
4473  */
4474 
4475 LABKEY.QueryWebPart.prototype.render = LABKEY.DataRegion.prototype.render;
4476 
4477 /**
4478  * @returns {LABKEY.DataRegion}
4479  */
4480 LABKEY.QueryWebPart.prototype.getDataRegion = LABKEY.DataRegion.prototype.getDataRegion;
4481 
4482 LABKEY.AggregateTypes = {
4483     /**
4484      * Displays the sum of the values in the specified column
4485      */
4486     SUM: 'sum',
4487     /**
4488      * Displays the mean of the values in the specified column
4489      */
4490     MEAN: 'mean',
4491     /**
4492      * Displays the count of the non-blank values in the specified column
4493      */
4494     COUNT: 'count',
4495     /**
4496      * Displays the maximum value from the specified column
4497      */
4498     MIN: 'min',
4499     /**
4500      * Displays the minimum values from the specified column
4501      */
4502     MAX: 'max',
4503 
4504     /**
4505      * Deprecated
4506      */
4507     AVG: 'mean'
4508 
4509     // TODO how to allow premium module additions to aggregate types?
4510 };