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