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