1 /**
  2  * @fileOverview
  3  * @author <a href="https://www.labkey.org">LabKey</a> (<a href="mailto:info@labkey.com">info@labkey.com</a>)
  4  * @license Copyright (c) 2008-2016 LabKey Corporation
  5  * <p/>
  6  * Licensed under the Apache License, Version 2.0 (the "License");
  7  * you may not use this file except in compliance with the License.
  8  * You may obtain a copy of the License at
  9  * <p/>
 10  * http://www.apache.org/licenses/LICENSE-2.0
 11  * <p/>
 12  * Unless required by applicable law or agreed to in writing, software
 13  * distributed under the License is distributed on an "AS IS" BASIS,
 14  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 15  * See the License for the specific language governing permissions and
 16  * limitations under the License.
 17  * <p/>
 18  */
 19 
 20 var console = console || {warn : function(){}};
 21 
 22 /**
 23  * @namespace Utils static class to provide miscellaneous utility functions.
 24  */
 25 LABKEY.Utils = new function()
 26 {
 27     // Private array of chars to use for UUID generation
 28     var CHARS = '0123456789abcdefghijklmnopqrstuvwxyz'.split('');
 29     var idSeed = 100;
 30 
 31     //When using Ext dateFields you can use DATEALTFORMATS for the altFormat: config option.
 32     var DATEALTFORMATS_Either =
 33             'j-M-y g:i a|j-M-Y g:i a|j-M-y G:i|j-M-Y G:i|' +
 34             'j-M-y|j-M-Y|' +
 35             'Y-n-d H:i:s|Y-n-d|' +
 36             'Y/n/d H:i:s|Y/n/d|' +
 37             'j M Y G:i:s O|' +  // 10 Sep 2009 11:24:12 -0700
 38             'j M Y H:i:s|c';
 39     var DATEALTFORMATS_MonthDay =
 40             'n/j/y g:i:s a|n/j/Y g:i:s a|n/j/y G:i:s|n/j/Y G:i:s|' +
 41             'n-j-y g:i:s a|n-j-Y g:i:s a|n-j-y G:i:s|n-j-Y G:i:s|' +
 42             'n/j/y g:i a|n/j/Y g:i a|n/j/y G:i|n/j/Y G:i|' +
 43             'n-j-y g:i a|n-j-Y g:i a|n-j-y G:i|n-j-Y G:i|' +
 44             'n/j/y|n/j/Y|' +
 45             'n-j-y|n-j-Y|' + DATEALTFORMATS_Either;
 46     var DATEALTFORMATS_DayMonth =
 47             'j/n/y g:i:s a|j/n/Y g:i:s a|j/n/y G:i:s|j/n/Y G:i:s|' +
 48             'j-n-y g:i:s a|j-n-Y g:i:s a|j-n-y G:i:s|j-n-Y G:i:s|' +
 49             'j/n/y g:i a|j/n/Y g:i a|j/n/y G:i|j/n/Y G:i|' +
 50             'j-n-y g:i a|j-n-Y g:i a|j-n-y G:i|j-n-Y G:i|' +
 51             'j/n/y|j/n/Y|' +
 52             'j-n-y|j-n-Y|' +
 53             'j-M-y|j-M-Y|' + DATEALTFORMATS_Either;
 54 
 55     function isObject(v)
 56     {
 57         return typeof v == "object" && Object.prototype.toString.call(v) === '[object Object]';
 58     }
 59 
 60 
 61     function _copy(o, depth)
 62     {
 63         if (depth==0 || !isObject(o))
 64             return o;
 65         var copy = {};
 66         for (var key in o)
 67             copy[key] = _copy(o[key], depth-1);
 68         return copy;
 69     }
 70 
 71 
 72     // like a general version of Ext.apply() or mootools.merge()
 73     function _merge(to, from, overwrite, depth)
 74     {
 75         for (var key in from)
 76         {
 77             if (from.hasOwnProperty(key))
 78             {
 79                 if (isObject(to[key]) && isObject(from[key]))
 80                 {
 81                     _merge(to[key], from[key], overwrite, depth-1);
 82                 }
 83                 else if (undefined === to[key] || overwrite)
 84                 {
 85                     to[key] = _copy(from[key], depth-1);
 86                 }
 87             }
 88         }
 89     }
 90 
 91     var getNextRow = function(rowElem)
 92     {
 93         if (null == rowElem)
 94             return null;
 95 
 96 
 97         var nextRow = rowElem.nextSibling;
 98         while (nextRow != null && !nextRow.tagName)
 99             nextRow = nextRow.nextSibling;
100 
101         if (nextRow == null || nextRow.tagName != "TR")
102             return null;
103 
104         return nextRow;
105     };
106 
107     var collapseExpand = function(elem, notify)
108     {
109         var collapse = false;
110         var url = elem.href;
111         while (elem.tagName != 'TR')
112             elem = elem.parentNode;
113 
114         var nextRow = getNextRow(elem);
115         if (null != nextRow && nextRow.style.display != "none")
116             collapse = true;
117 
118         while (nextRow != null)
119         {
120             if (nextRow.className.indexOf("labkey-header") != -1)
121                 break;
122             if (nextRow.style.display != "none")
123                 nextRow.style.display = "none";
124             else
125                 nextRow.style.display = "";
126             nextRow = getNextRow(nextRow);
127         }
128 
129         if (null != url && notify)
130             notifyExpandCollapse(url, collapse);
131         return false;
132     };
133 
134     var notifyExpandCollapse = function(url, collapse)
135     {
136         if (collapse)
137             url += "&collapse=true";
138         LABKEY.Ajax.request({url: url});
139     };
140 
141     var toggleLink = function(link, notify)
142     {
143         collapseExpand(link, notify);
144         var i = 0;
145         while (typeof(link.childNodes[i].src) == "undefined" )
146             i++;
147 
148         if (link.childNodes[i].src.search("plus.gif") >= 0)
149             link.childNodes[i].src = link.childNodes[i].src.replace("plus.gif", "minus.gif");
150         else
151             link.childNodes[i].src = link.childNodes[i].src.replace("minus.gif", "plus.gif");
152         return false;
153     };
154 
155     /** @scope LABKEY.Utils */
156     return {
157         /**
158         * Encodes the html passed in so that it will not be interpreted as HTML by the browser.
159         * For example, if your input string was "<p>Hello</p>" the output would be
160         * "&lt;p&gt;Hello&lt;/p&gt;". If you set an element's innerHTML property
161         * to this string, the HTML markup will be displayed as literal text rather than being
162         * interpreted as HTML.
163         *
164         * @param {String} html The HTML to encode
165 		* @return {String} The encoded HTML
166 		*/
167         encodeHtml : function(html)
168         {
169             // http://stackoverflow.com/questions/1219860/html-encoding-in-javascript-jquery
170             return String(html)
171                     .replace(/&/g, '&')
172                     .replace(/"/g, '"')
173                     .replace(/'/g, ''')
174                     .replace(/</g, '<')
175                     .replace(/>/g, '>');
176         },
177 
178         isArray: function(value) {
179             // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray
180             return Object.prototype.toString.call(value) === "[object Array]";
181         },
182 
183         isDate: function(value) {
184             return Object.prototype.toString.call(value) === '[object Date]';
185         },
186 
187         isDefined: function(value) {
188             return typeof value !== 'undefined';
189         },
190 
191         isFunction: function(value) {
192             // http://stackoverflow.com/questions/5999998/how-can-i-check-if-a-javascript-variable-is-function-type
193             var getType = {};
194             return value && getType.toString.call(value) === '[object Function]';
195         },
196 
197         isObject: isObject,
198 
199         /**
200          * Returns date formats for use in an Ext.form.DateField. Useful when using a DateField in an Ext object,
201          * it contains a very large set of date formats, which helps make a DateField more robust. For example, a
202          * user would be allowed to enter dates like 6/1/2011, 06/01/2011, 6/1/11, etc.
203          */
204         getDateAltFormats : function()
205         {
206             return LABKEY.useMDYDateParsing ? DATEALTFORMATS_MonthDay : DATEALTFORMATS_DayMonth;
207         },
208 
209         displayAjaxErrorResponse: function() {
210             console.warn('displayAjaxErrorResponse: This is just a stub implementation, request the dom version of the client API : clientapi_dom.lib.xml to get the concrete implementation');
211         },
212 
213         /**
214          * Generates a display string from the response to an error from an AJAX request
215          * @private
216          * @ignore
217          * @param {XMLHttpRequest} responseObj The XMLHttpRequest object containing the response data.
218          * @param {Error} [exceptionObj] A JavaScript Error object caught by the calling code.
219          * @param {Object} [config] An object with additional configuration properties.  It supports the following:
220          * <li>msgPrefix: A string that will be used as a prefix in the error message.  Default to: 'An error occurred trying to load'</li>
221          * <li>showExceptionClass: A boolean flag to display the java class of the exception.</li>
222          */
223         getMsgFromError: function(responseObj, exceptionObj, config){
224             config = config || {};
225             var error;
226             var prefix = config.msgPrefix || 'An error occurred trying to load:\n';
227 
228             if (responseObj &&
229                 responseObj.responseText &&
230                 responseObj.getResponseHeader('Content-Type') &&
231                 responseObj.getResponseHeader('Content-Type').indexOf('application/json') >= 0)
232             {
233                 var jsonResponse = LABKEY.Utils.decode(responseObj.responseText);
234                 if (jsonResponse && jsonResponse.exception)
235                 {
236                     error = prefix + jsonResponse.exception;
237                     if (config.showExceptionClass)
238                         error += "\n(" + (jsonResponse.exceptionClass ? jsonResponse.exceptionClass : "Exception class unknown") + ")";
239                 }
240             }
241             if (!error)
242                 error = prefix + "Status: " + responseObj.statusText + " (" + responseObj.status + ")";
243             if (exceptionObj && exceptionObj.message)
244                 error += "\n" + exceptionObj.name + ": " + exceptionObj.message;
245 
246             return error;
247         },
248 
249         /**
250          * This method has been migrated to specific instances for both Ext 3.4.1 and Ext 4.2.1.
251          * For Ext 3.4.1 see LABKEY.ext.Utils.resizeToViewport
252          * For Ext 4.2.1 see LABKEY.ext4.Util.resizeToViewport
253          */
254         resizeToViewport : function()
255         {
256             console.warn('LABKEY.Utils.resizeToViewport has been migrated. See JavaScript API documentation for details.');
257         },
258 
259         /**
260          * Returns a URL to the appropriate file icon image based on the specified file name.
261          * Note that file name can be a full path or just the file name and extension.
262          * If the file name does not include an extension, the URL for a generic image will be returned
263          * @param {String} fileName The file name.
264          * @return {String} The URL suitable for use in the src attribute of an img element.
265          */
266         getFileIconUrl : function(fileName) {
267             var idx = fileName.lastIndexOf(".");
268             var extension = (idx >= 0) ? fileName.substring(idx + 1) : "_generic";
269             return LABKEY.ActionURL.buildURL("core", "getAttachmentIcon", "", {extension: extension});
270         },
271 
272         /**
273          * This is used internally by other class methods to automatically parse returned JSON
274          * and call another success function passing that parsed JSON.
275          * @param fn The callback function to wrap
276          * @param scope The scope for the callback function
277          * @param {boolean} [isErrorCallback=false] Set to true if the function is an error callback.  If true, and you do not provide a separate callback, alert will popup showing the error message
278          */
279         getCallbackWrapper : function(fn, scope, isErrorCallback) {
280             return function(response, options)
281             {
282                 var json = response.responseJSON;
283                 if (!json)
284                 {
285                     //ensure response is JSON before trying to decode
286                     if (response && response.getResponseHeader && response.getResponseHeader('Content-Type')
287                             && response.getResponseHeader('Content-Type').indexOf('application/json') >= 0){
288                         try {
289                             json = LABKEY.Utils.decode(response.responseText);
290                         }
291                         catch (error){
292                             //we still want to proceed even if we cannot decode the JSON
293                         }
294 
295                     }
296 
297                     response.responseJSON = json;
298                 }
299 
300                 if (!json && isErrorCallback)
301                 {
302                     json = {};
303                 }
304 
305                 if (json && !json.exception && isErrorCallback)
306                 {
307                     // Try to make sure we don't show an empty error message
308                     json.exception = (response && response.statusText ? response.statusText : "Communication failure.");
309                 }
310 
311                 if (fn)
312                     fn.call(scope || this, json, response, options);
313                 else if (isErrorCallback && response.status != 0)
314                 {
315                     // Don't show an error dialog if the user cancelled the request in the browser, like navigating
316                     // to another page
317                     LABKEY.Utils.alert("Error", json.exception);
318                 }
319             };
320         },
321 
322         /**
323          * Applies properties from the source object to the target object, translating
324          * the property names based on the translation map. The translation map should
325          * have an entry per property that you wish to rename when it is applied on
326          * the target object. The key should be the name of the property on the source object
327          * and the value should be the desired name on the target object. The value may
328          * also be set to null or false to prohibit that property from being applied.
329          * By default, this function will also apply all other properties on the source
330          * object that are not listed in the translation map, but you can override this
331          * by supplying false for the applyOthers paramer.
332          * @param target The target object
333          * @param source The source object
334          * @param translationMap A map listing property name translations
335          * @param applyOthers Set to false to prohibit application of properties
336          * not explicitly mentioned in the translation map.
337          * @param applyFunctions Set to false to prohibit application of properties
338          * that are functions
339          */
340         applyTranslated : function(target, source, translationMap, applyOthers, applyFunctions)
341         {
342             if (undefined === target)
343                 target = {};
344             if (undefined === applyOthers)
345                 applyOthers = true;
346             if (undefined == applyFunctions && applyOthers)
347                 applyFunctions = true;
348             var targetPropName;
349             for (var prop in source)
350             {
351                 //special case: Ext adds a "constructor" property to every object, which we don't want to apply
352                 if (prop == "constructor" || LABKEY.Utils.isFunction(prop))
353                     continue;
354                 
355                 targetPropName = translationMap[prop];
356                 if (targetPropName)
357                     target[translationMap[prop]] = source[prop];
358                 else if (undefined === targetPropName && applyOthers && (applyFunctions || !LABKEY.Utils.isFunction(source[prop])))
359                     target[prop] = source[prop];
360             }
361         },
362 
363         /**
364          * Sets a client-side cookie.  Useful for saving non-essential state to provide a better
365          * user experience.  Note that some browser settings may prevent cookies from being saved,
366          * and users can clear browser cookies at any time, so cookies are not a substitute for
367          * database persistence.
368          * @param {String} name The name of the cookie to be saved.
369          * @param {String} value The value of the cookie to be saved.
370          * @param {Boolean} pageonly Whether this cookie should be scoped to the entire site, or just this page.
371          * Page scoping considers the entire URL without parameters; all URL contents after the '?' are ignored.
372          * @param {int} days The number of days the cookie should be saved on the client.
373          */
374         setCookie : function(name, value, pageonly, days) {
375             var expires;
376             if (days)
377             {
378                 var date = new Date();
379                 date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
380                 expires = "; expires=" + date.toGMTString();
381             }
382             else
383                 expires = "";
384             var path = "/";
385             if (pageonly)
386                 path = location.pathname.substring(0, location.pathname.lastIndexOf('/'));
387             document.cookie = name + "=" + value + expires + "; path=" + path;
388         },
389 
390         /**
391          * Retrieves a cookie.  Useful for retrieving non-essential state to provide a better
392          * user experience.  Note that some browser settings may prevent cookies from being saved,
393          * and users can clear browser cookies at any time, so previously saved cookies should not be assumed
394          * to be available.
395          * @param {String} name The name of the cookie to be retrieved.
396          * @param {String} defaultvalue The value to be returned if no cookie with the specified name is found on the client.
397          */
398         getCookie : function(name, defaultvalue) {
399             var nameEQ = name + "=";
400             var ca = document.cookie.split(';');
401             for (var i=0; i < ca.length; i++)
402             {
403                 var c = ca[i];
404                 while (c.charAt(0) == ' ')
405                     c = c.substring(1, c.length);
406                 if (c.indexOf(nameEQ) == 0)
407                     return c.substring(nameEQ.length, c.length);
408             }
409             return defaultvalue;
410         },
411 
412         /**
413          * Retrieves the current LabKey Server session ID. Note that this may only be made available when the
414          * session ID cookie is marked as httpOnly = false.
415          * @returns {String} sessionid The current session id. Defaults to ''.
416          * @see {@link https://www.owasp.org/index.php/HttpOnly|OWASP HttpOnly}
417          * @see {@link https://tomcat.apache.org/tomcat-7.0-doc/config/context.html#Common_Attributes|Tomcat Attributes}
418          */
419         getSessionID : function()
420         {
421             return LABKEY.Utils.getCookie('JSESSIONID', '');
422         },
423 
424         /**
425          * Deletes a cookie.  Note that 'name' and 'pageonly' should be exactly the same as when the cookie
426          * was set.
427          * @param {String} name The name of the cookie to be deleted.
428          * @param {Boolean} pageonly Whether the cookie is scoped to the entire site, or just this page.
429          * Deleting a site-level cookie has no impact on page-level cookies, and deleting page-level cookies
430          * has no impact on site-level cookies, even if the cookies have the same name.
431          */
432         deleteCookie : function (name, pageonly)
433         {
434             LABKEY.Utils.setCookie(name, "", pageonly, -1);
435         },
436 
437         /**
438          * Loads JavaScript file(s) from the server.
439          * @function
440          * @param {(string|string[])} file - A file or Array of files to load.
441          * @param {Function} [callback] - Callback for when all dependencies are loaded.
442          * @param {Object} [scope] - Scope of callback.
443          * @param {boolean} [inOrder=false] - True to load the scripts in the order they are passed in. Default is false.
444          * @example
445          <script type="text/javascript">
446          LABKEY.requiresScript("myModule/myScript.js", true, function() {
447                     // your script is loaded
448                 });
449          </script>
450          */
451         requiresScript : function(file, callback, scope, inOrder)
452         {
453             LABKEY.requiresScript.apply(this, arguments);
454         },
455 
456         /**
457          * Includes a Cascading Style Sheet (CSS) file into the page. If the file was already included by some other code, this
458          * function will simply ignore the call. This may be used to include CSS files defined in your module's web/ directory.
459          * @param {String} filePath The path to the script file to include. This path should be relative to the web application
460          * root. So for example, if you wanted to include a file in your module's web/mymodule/styles/ directory,
461          * the path would be "mymodule/styles/mystyles.css"
462          */
463         requiresCSS : function(filePath)
464         {
465             LABKEY.requiresCss(filePath);
466         },
467 
468         /**
469          * Returns true if value ends with ending
470          * @param value the value to examine
471          * @param ending the ending to look for
472          */
473         endsWith : function(value, ending)
474         {
475             if (!value || !ending)
476                 return false;
477             if (value.length < ending.length)
478                 return false;
479             return value.substring(value.length - ending.length) == ending;
480         },
481 
482         /**
483          * Iteratively calls a tester function you provide, calling another callback function once the
484          * tester function returns true. This function is useful for advanced JavaScript scenarios, such
485          * as cases where you are including common script files dynamically using the requiresScript()
486          * method, and need to wait until classes defined in those files are parsed and ready for use.
487          *  
488          * @param {Object} config a configuration object with the following properties:
489          * @param {Function} config.testCallback A function that returns true or false. This will be called every
490          * ten milliseconds until it returns true or the maximum number of tests have been made.
491          * @param {Array} [config.testArguments] A array of arguments to pass to the testCallback function
492          * @param {Function} config.success The function to call when the testCallback returns true.
493          * @param {Array} [config.successArguments] A array of arguments to pass to the successCallback function
494          * @param {Object} [config.failure] A function to call when the testCallback throws an exception, or when
495          * the maximum number of tests have been made.
496          * @param {Array} [config.errorArguments] A array of arguments to pass to the errorCallback function
497          * @param {Object} [config.scope] A scope to use when calling any of the callback methods (defaults to this)
498          * @param {int} [config.maxTests] Maximum number of tests before the errorCallback is called (defaults to 1000).
499          *
500          * @example
501 <script>
502     LABKEY.Utils.requiresScript("FileUploadField.js");
503     LABKEY.Utils.requiresCSS("FileUploadField.css");
504 </script>
505 
506 <script>
507     function tester()
508     {
509         return undefined != Ext.form.FileUploadField;
510     }
511 
512     function onTrue(msg)
513     {
514         //this alert is merely to demonstrate the successArguments config property
515         alert(msg);
516 
517         //use the file upload field...
518     }
519 
520     function onFailure(msg)
521     {
522         alert("ERROR: " + msg);
523     }
524 
525     LABKEY.Utils.onTrue({
526         testCallback: tester,
527         success: onTrue,
528         successArguments: ['FileUploadField is ready to use!'],
529         failure: onFailure,
530         maxTests: 100
531     });
532 </script>
533          */
534         onTrue : function(config) {
535             config.maxTests = config.maxTests || 1000;
536             try
537             {
538                 if (config.testCallback.apply(config.scope || this, config.testArguments || []))
539                     LABKEY.Utils.getOnSuccess(config).apply(config.scope || this, config.successArguments || []);
540                 else
541                 {
542                     if (config.maxTests <= 0)
543                     {
544                         throw "Maximum number of tests reached!";
545                     }
546                     else
547                     {
548                         --config.maxTests;
549                         LABKEY.Utils.onTrue.defer(10, this, [config]);
550                     }
551                 }
552             }
553             catch(e)
554             {
555                 if (LABKEY.Utils.getOnFailure(config))
556                 {
557                     LABKEY.Utils.getOnFailure(config).apply(config.scope || this, [e,config.errorArguments]);
558                 }
559             }
560         },
561 
562         ensureBoxVisible : function() {
563             console.warn('LABKEY.Utils.ensureBoxVisible has been migrated to the appropriate Ext scope. Consider LABKEY.ext.Utils.ensureBoxVisible or LABKEY.ext4.Util.ensureBoxVisible');
564         },
565 
566         /**
567          * Will generate a unique id. If you provide a prefix, consider making it DOM safe so it can be used as
568          * an element id.
569          * @param {string} [prefix=lk-gen] - Optional prefix to start the identifier.
570          * @returns {*}
571          */
572         id : function(prefix) {
573             return (prefix || "lk-gen") + (++idSeed);
574         },
575 
576         /**
577           * Returns a universally unique identifier, of the general form: "92329D39-6F5C-4520-ABFC-AAB64544E172"
578           * NOTE: Do not use this for DOM id's as it does not meet the requirements for DOM id specification.
579           * Based on original Math.uuid.js (v1.4)
580           * http://www.broofa.com
581           * mailto:robert@broofa.com
582           * Copyright (c) 2010 Robert Kieffer
583           * Dual licensed under the MIT and GPL licenses.
584         */
585         generateUUID : function() {
586             // First see if there are any server-generated UUIDs available to return
587             if (LABKEY && LABKEY.uuids && LABKEY.uuids.length > 0)
588             {
589                 return LABKEY.uuids.pop();
590             }
591             // From the original Math.uuidFast implementation
592             var chars = CHARS, uuid = new Array(36), rnd = 0, r;
593             for (var i = 0; i < 36; i++)
594             {
595                 if (i == 8 || i == 13 || i == 18 || i == 23)
596                 {
597                     uuid[i] = '-';
598                 }
599                 else if (i == 14)
600                 {
601                     uuid[i] = '4';
602                 }
603                 else
604                 {
605                     if (rnd <= 0x02) rnd = 0x2000000 + (Math.random() * 0x1000000) | 0;
606                     r = rnd & 0xf;
607                     rnd = rnd >> 4;
608                     uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r];
609                 }
610             }
611             return uuid.join('');
612         },
613 
614         /**
615          * Returns a string containing a well-formed html anchor that will apply theme specific styling. The configuration
616          * takes any property value pair and places them on the anchor.
617          * @param {Object} config a configuration object that models html anchor properties:
618          * @param {String} config.href (required if config.onClick not specified) the reference the anchor will use.
619          * @param {String} config.onClick (required if config.href not specified) script called when the onClick event is fired by
620          * the anchor.
621          * @param {String} config.text text that is rendered inside the anchor element.
622          */
623         textLink : function(config)
624         {
625             if (config.href === undefined && !config.onClick === undefined)
626             {
627                 throw "href AND/OR onClick required in call to LABKEY.Utils.textLink()";
628             }
629             var attrs = " ";
630             if (config)
631             {
632                 for (var i in config)
633                 {
634                     if (config.hasOwnProperty(i))
635                     {
636                         if (i.toString() != "text" && i.toString() != "class")
637                         {
638                             attrs += i.toString() + '=\"' + config[i] + '\" ';
639                         }
640                     }
641                 }
642 
643                 return '<a class="labkey-text-link"' + attrs + '>' + (config.text != null ? config.text : "") + '</a>';
644             }
645             throw "Config object not found for textLink.";
646         },
647 
648         /**
649          *
650          * Standard documented name for error callback arguments is "failure" but various other names have been employed in past.
651          * This function provides reverse compatibility by picking the failure callback argument out of a config object
652          * be it named failure, failureCallback or errorCallback.
653          *
654          * @param config
655          */
656         getOnFailure : function(config)
657         {
658             return config.failure || config.errorCallback || config.failureCallback;
659             // maybe it be desirable for this fall all the way back to returning LABKEY.Utils.displayAjaxErrorResponse?
660         },
661 
662         /**
663          *
664          * Standard documented name for success callback arguments is "success" but various names have been employed in past.
665          * This function provides reverse compatibility by picking the success callback argument out of a config object,
666          * be it named success or successCallback.
667          *
668          * @param config
669          */
670         getOnSuccess : function(config)
671         {
672             return config.success || config.successCallback
673         },
674 
675 
676         /**
677          * Apply properties from b, c, ...  to a.  Properties of each subsequent
678          * object overwrites the previous.
679          *
680          * The first object is modified.
681          *
682          * Use merge({}, o) to create a deep copy of o.
683          */
684         merge : function(a, b, c)
685         {
686             var o = a;
687             for (var i=1 ; i<arguments.length ; i++)
688                 _merge(o, arguments[i], true, 50);
689             return o;
690         },
691 
692 
693         /**
694          * Apply properties from b, c, ... to a.  Properties are not overwritten.
695          *
696          * The first object is modified.
697          */
698         mergeIf : function(a, b, c)
699         {
700             var o = arguments[0];
701             for (var i=1 ; i<arguments.length ; i++)
702                 _merge(o, arguments[i], false, 50);
703             return o;
704         },
705 
706         onError : function(error){
707             console.warn('onError: This is just a stub implementation, request the dom version of the client API : clientapi_dom.lib.xml to get the concrete implementation');
708         },
709 
710         /**
711          * Returns true if the passed object is empty (ie. {}) and false if not.
712          *
713          * @param {Object} ob The object to test
714          * @return {Boolean} the result of the test
715         */
716         isEmptyObj : function(ob){
717            for (var i in ob){ return false;}
718            return true;
719         },
720 
721         /**
722          * Rounds the passed number to the specified number of decimals
723          *
724          * @param {Number} input The number to round
725          * @param {Number} dec The number of decimal places to use
726          * @return {Number} The rounded number
727         */
728         roundNumber : function(input, dec){
729             return Math.round(input*Math.pow(10,dec))/Math.pow(10,dec);
730         },
731 
732         /**
733          * Will pad the input string with zeros to the desired length.
734          *
735          * @param {Number/String} input The input string / number
736          * @param {Integer} length The desired length
737          * @param {String} padChar The character to use for padding.
738          * @return {String} The padded string
739         **/
740         padString : function(input, length, padChar){
741             if (typeof input != 'string')
742                 input = input.toString();
743 
744             var pd = '';
745             if (length > input.length){
746                 for (var i=0; i < (length-input.length); i++){
747                     pd += padChar;
748                 }
749             }
750             return pd + input;
751         },
752 
753         /**
754          * Returns true if the arguments are case-insensitive equal.  Note: the method converts arguments to strings for the purposes of comparing numbers, which means that it will return odd behaviors with objects (ie. LABKEY.Utils.caseInsensitiveEquals({t: 3}, '[object Object]') returns true)
755          *
756          * @param {String/Number} a The first item to test
757          * @param {String/Number} b The second item to test
758          * @return {boolean} True if arguments are case-insensitive equal, false if not
759         */
760         caseInsensitiveEquals: function(a, b) {
761             return String(a).toLowerCase() == String(b).toLowerCase();
762         },
763 
764         /**
765          * Tests whether the passed value can be used as boolean, using a loose definition.  Acceptable values for true are: 'true', 'yes', 1, 'on' or 't'.  Acceptable values for false are: 'false', 'no', 0, 'off' or 'f'.  Values are case-insensitive.
766          * @param value The value to test
767          */
768         isBoolean: function(value){
769             var upperVal = value.toString().toUpperCase();
770             if (upperVal == "TRUE" || value == "1" || upperVal == "Y" || upperVal == "YES" || upperVal == "ON" || upperVal == "T"
771                     || upperVal == "FALSE" || value == "0" || upperVal == "N" || upperVal == "NO" || upperVal == "OFF" || upperVal == "F"){
772                 return true;
773             }
774         },
775 
776         /**
777          * Returns true if the passed value is a string.
778          * @param {Object} value The value to test
779          * @return {Boolean}
780          */
781         isString: function(value) {
782             return typeof value === 'string';
783         },
784 
785         onReady: function(config) {
786             console.warn('onReady: This is just a stub implementation, request the dom version of the client API : clientapi_dom.lib.xml to get the concrete implementation');
787         },
788 
789         /**
790          * Decodes (parses) a JSON string to an object.
791          *
792          * @param {String} data The JSON string
793          * @return {Object} The resulting object
794          */
795         decode : function(data) {
796             return JSON.parse(data + "");
797         },
798 
799         /**
800          * Encodes an Object to a string.
801          *
802          * @param {Object} data the variable to encode.
803          * @return {String} The JSON string.
804          */
805         encode : function(data) {
806             return JSON.stringify(data);
807         },
808 
809         /**
810          * Applies config properties to the specified object.
811          * @param object
812          * @param config
813          * @returns {*}
814          */
815         apply : function(object, config) {
816             var enumerables = ['hasOwnProperty', 'valueOf', 'isPrototypeOf', 'propertyIsEnumerable',
817                 'toLocaleString', 'toString', 'constructor'];
818 
819             if (object && config && typeof config === 'object') {
820                 var i, j, k;
821 
822                 for (i in config) {
823                     object[i] = config[i];
824                 }
825 
826                 if (enumerables) {
827                     for (j = enumerables.length; j--;) {
828                         k = enumerables[j];
829                         if (config.hasOwnProperty(k)) {
830                             object[k] = config[k];
831                         }
832                     }
833                 }
834             }
835             return object;
836         },
837 
838         /**
839          * Display an error dialog
840          * @param title
841          * @param msg
842          */
843         alert : function(title, msg) {
844             console.warn('alert: This is just a stub implementation, request the dom version of the client API : clientapi_dom.lib.xml to get the concrete implementation');
845             console.warn(title + ":", msg);
846         },
847 
848 
849         escapeRe : function(s) {
850             return s.replace(/([-.*+?\^${}()|\[\]\/\\])/g, "\\$1");
851         },
852 
853         getMeasureAlias : function(measure, override) {
854             if (measure.alias && !override) {
855                 return measure.alias;
856             }
857             else {
858                 var alias = measure.schemaName + '_' + measure.queryName + '_' + measure.name;
859                 return alias.replace(/\//g, '_');
860             }
861         },
862 
863         // private
864         collapseExpand: collapseExpand,
865         notifyExpandCollapse: notifyExpandCollapse,
866         toggleLink: toggleLink
867     };
868 };
869 
870 /** docs for methods defined in dom/Utils.js - primarily here to ensure API docs get generated with combined core/dom versions */
871 
872 /**
873  * Sends a JSON object to the server which turns it into an Excel file and returns it to the browser to be saved or opened.
874  * @memberOf LABKEY.Utils
875  * @function
876  * @static
877  * @name convertToExcel
878  * @param {Object} spreadsheet the JavaScript representation of the data
879  * @param {String} spreadsheet.fileName name to suggest to the browser for saving the file. If the fileName is
880  * specified and ends with ".xlsx", it will be returned in Excel 2007 format.
881  * @param {String} spreadsheet.sheets array of sheets, which are objects with properties:
882  * <ul>
883  * <li><b>name:</b> name of the Excel sheet</li>
884  * <li><b>data:</b> two dimensional array of values</li>
885  * </ul>
886  * The value array may be either primitives (booleans, numbers, Strings, and dates), or may be a map with
887  * the following structure:
888  * <ul>
889  * <li><b>value:</b> the boolean, number, String, or date value of the cell</li>
890  * <li><b>formatString:</b> for dates and numbers, the Java format string used with SimpleDateFormat
891  * or DecimalFormat to control how the value is formatted</li>
892  * <li><b>timeOnly:</b> for dates, whether the date part should be ignored and only the time value is important</li>
893  * <li><b>forceString:</b> force the value to be treated as a string (i.e. prevent attempt to convert it to a date)</li>
894  * </ul>
895  * @example <script type="text/javascript">
896  LABKEY.Utils.convertToExcel(
897  {
898      fileName: 'output.xls',
899      sheets:
900      [
901          {
902              name: 'FirstSheet',
903              data:
904              [
905                  ['Row1Col1', 'Row1Col2'],
906                  ['Row2Col1', 'Row2Col2']
907              ]
908          },
909          {
910              name: 'SecondSheet',
911              data:
912              [
913                  ['Col1Header', 'Col2Header'],
914                  [{value: 1000.5, formatString: '0,000.00'}, {value: '5 Mar 2009 05:14:17', formatString: 'yyyy MMM dd'}],
915                  [{value: 2000.6, formatString: '0,000.00'}, {value: '6 Mar 2009 07:17:10', formatString: 'yyyy MMM dd'}]
916 
917              ]
918          }
919      ]
920  });
921  </script>
922  */
923 
924 /**
925  * Sends a JSON object to the server which turns it into an TSV or CSV file and returns it to the browser to be saved or opened.
926  * @memberOf LABKEY.Utils
927  * @function
928  * @static
929  * @name convertToTable
930  * @param {Object} config.  The config object
931  * @param {String} config.fileNamePrefix name to suggest to the browser for saving the file. The appropriate extension (either ".txt" or ".csv", will be appended based on the delim character used (see below).  Defaults to 'Export'
932  * @param {String} config.delim The separator between fields.  Allowable values are 'COMMA' or 'TAB'.
933  * @param {String} config.quoteChar The character that will be used to quote each field.  Allowable values are 'DOUBLE' (ie. double-quote character), 'SINLGE' (ie. single-quote character) or 'NONE' (ie. no character used).  Defaults to none.
934  * @param {String} config.newlineChar The character that will be used to separate each line.  Defaults to '\n'
935  * @param {String} config.rows array of rows, which are arrays with values for each cell.
936  * @example <script type="text/javascript">
937  LABKEY.Utils.convertToTable(
938  {
939      fileName: 'output.csv',
940      rows:
941      [
942          ['Row1Col1', 'Row1Col2'],
943          ['Row2Col1', 'Row2Col2']
944      ],
945      delim: 'COMMA'
946  });
947  </script>
948  */
949 
950 /**
951  * Display an error dialog
952  * @memberOf LABKEY.Utils
953  * @function
954  * @static
955  * @name alert
956  * @param title
957  * @param msg
958  */
959 
960 /**
961  * Sets the title of the webpart on the page.  This change is not sticky, so it will be reverted on refresh.
962  * @memberOf LABKEY.Utils
963  * @function
964  * @static
965  * @name setWebpartTitle
966  * @param {string} title The title string
967  * @param {integer} webPartId The ID of the webpart
968  */
969 
970 /**
971  * Provides a generic error callback.  This helper show a modal dialog, log the error to the console
972  * and will log the error to the audit log table. The user must have insert permissions on the selected container for
973  * this to work.  By default, it will insert the error into the Shared project.  A containerPath param can be passed to
974  * use a different container.  The intent of this helper is to provide site admins with a mechanism to identify errors associated
975  * with client-side code.  If noAuditLog=true is used, the helper will not log the error.
976  *
977  * @memberOf LABKEY.Utils
978  * @function
979  * @static
980  * @name onError
981  * @param {Object} error The error object passed to the callback function
982  * @param {String} [error.containerPath] Container where errors will be logged. Defaults to /shared
983  * @param {Boolean} [error.noAuditLog] If false, the errors will not be logged in the audit table.  Defaults to true
984  *
985  * @example <script type="text/javascript">
986  //basic usage
987  LABKEY.Query.selectRows({
988                 schemaName: 'core',
989                 queryName: 'users',
990                 success: function(){},
991                 failure: LABKEY.Utils.onError
992             });
993 
994  //custom container and turning off logging
995  LABKEY.Query.selectRows({
996                 schemaName: 'core',
997                 queryName: 'users',
998                 success: function(){},
999                 failure: function(error){
1000                      error.containerPath = 'myContainer';
1001                      error.noAuditLog = true;
1002                      LABKEY.Utils.onError(error);
1003                 }
1004             });
1005  </script>
1006  */
1007 
1008 /**
1009  * Shows an error dialog box to the user in response to an error from an AJAX request, including
1010  * any error messages from the server.
1011  *
1012  * @memberOf LABKEY.Utils
1013  * @function
1014  * @static
1015  * @name displayAjaxErrorResponse
1016  * @param {XMLHttpRequest} responseObj The XMLHttpRequest object containing the response data.
1017  * @param {Error} [exceptionObj] A JavaScript Error object caught by the calling code.
1018  * @param {boolean} [showExceptionClass] Flag to display the java class of the exception.
1019  * @param {String} [msgPrefix] Prefix to the error message (defaults to: 'An error occurred trying to load:')
1020  * The error dialog will display the Error's name and message, if available.
1021  */
1022 
1023 /**
1024  * Adds new listener to be executed when all required scripts are fully loaded.
1025  * @memberOf LABKEY.Utils
1026  * @function
1027  * @static
1028  * @name onReady
1029  * @param {Mixed} config Either a callback function, or an object with the following properties:
1030  *
1031  * <li>callback (required) A function that will be called when required scripts are loaded.</li>
1032  * <li>scope (optional) The scope to be used for the callback function.  Defaults to the current scope.</li>
1033  * <li>scripts (optional) A string with a single script or an array of script names to load.  This will be passed to LABKEY.requiresScript().</li>
1034  * @example <script type="text/javascript">
1035  //simple usage
1036  LABKEY.onReady(function(){
1037                     //your code here.  will be executed once scripts have loaded
1038                 });
1039 
1040  //
1041  LABKEY.Utils.onReady({
1042                     scope: this,
1043                     scripts: ['/myModule/myScript.js', 'AnotherScript.js],
1044                     callback: function(){
1045                         //your code here.  will be executed once scripts have loaded
1046                     });
1047                 });
1048  </script>
1049  */
1050