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