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