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) 2014-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 LABKEY.Utils = new function(impl, $) { 21 22 // Insert a hidden <form> into to page, put the JSON into it, and submit it - the server's response will 23 // make the browser pop up a dialog 24 var formSubmit = function(url, value) 25 { 26 var formId = LABKEY.Utils.generateUUID(); 27 var formHTML = '<form method="POST" id="' + formId + '" action="' + url + '">' + 28 '<input type="hidden" name="json" value="' + LABKEY.Utils.encodeHtml(LABKEY.Utils.encode(value)) + '" />' + 29 '</form>'; 30 $('body').append(formHTML); 31 $('#'+formId).submit(); 32 }; 33 34 /** 35 * Shows an error dialog box to the user in response to an error from an AJAX request, including 36 * any error messages from the server. 37 * @param {XMLHttpRequest} responseObj The XMLHttpRequest object containing the response data. 38 * @param {Error} [exceptionObj] A JavaScript Error object caught by the calling code. 39 * @param {boolean} [showExceptionClass] Flag to display the java class of the exception. 40 * @param {String} [msgPrefix] Prefix to the error message (defaults to: 'An error occurred trying to load:') 41 * The error dialog will display the Error's name and message, if available. 42 */ 43 impl.displayAjaxErrorResponse = function(responseObj, exceptionObj, showExceptionClass, msgPrefix) 44 { 45 if (responseObj.status == 0) 46 { 47 // Don't show an error dialog if the user cancelled the request in the browser, like navigating 48 // to another page 49 return; 50 } 51 52 var error = LABKEY.Utils.getMsgFromError(responseObj, exceptionObj, { 53 msgPrefix: msgPrefix, 54 showExceptionClass: showExceptionClass 55 }); 56 LABKEY.Utils.alert("Error", error); 57 }; 58 59 /** 60 * 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. 61 * @param {Object} spreadsheet the JavaScript representation of the data 62 * @param {String} spreadsheet.fileName name to suggest to the browser for saving the file. If the fileName is 63 * specified and ends with ".xlsx", it will be returned in Excel 2007 format. 64 * @param {String} spreadsheet.sheets array of sheets, which are objects with properties: 65 * <ul> 66 * <li><b>name:</b> name of the Excel sheet</li> 67 * <li><b>data:</b> two dimensional array of values</li> 68 * </ul> 69 * The value array may be either primitives (booleans, numbers, Strings, and dates), or may be a map with 70 * the following structure: 71 * <ul> 72 * <li><b>value:</b> the boolean, number, String, or date value of the cell</li> 73 * <li><b>formatString:</b> for dates and numbers, the Java format string used with SimpleDateFormat 74 * or DecimalFormat to control how the value is formatted</li> 75 * <li><b>timeOnly:</b> for dates, whether the date part should be ignored and only the time value is important</li> 76 * <li><b>forceString:</b> force the value to be treated as a string (i.e. prevent attempt to convert it to a date)</li> 77 * </ul> 78 * @example <script type="text/javascript"> 79 LABKEY.Utils.convertToExcel( 80 { 81 fileName: 'output.xls', 82 sheets: 83 [ 84 { 85 name: 'FirstSheet', 86 data: 87 [ 88 ['Row1Col1', 'Row1Col2'], 89 ['Row2Col1', 'Row2Col2'] 90 ] 91 }, 92 { 93 name: 'SecondSheet', 94 data: 95 [ 96 ['Col1Header', 'Col2Header'], 97 [{value: 1000.5, formatString: '0,000.00'}, {value: '5 Mar 2009 05:14:17', formatString: 'yyyy MMM dd'}], 98 [{value: 2000.6, formatString: '0,000.00'}, {value: '6 Mar 2009 07:17:10', formatString: 'yyyy MMM dd'}] 99 100 ] 101 } 102 ] 103 }); 104 </script> 105 */ 106 impl.convertToExcel = function(spreadsheet) { 107 formSubmit(LABKEY.ActionURL.buildURL("experiment", "convertArraysToExcel"), spreadsheet); 108 }; 109 110 /** 111 * 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. 112 * @param {Object} config. The config object 113 * @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' 114 * @param {String} config.delim The separator between fields. Allowable values are 'COMMA' or 'TAB'. 115 * @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. 116 * @param {String} config.newlineChar The character that will be used to separate each line. Defaults to '\n' 117 * @param {String} config.rows array of rows, which are arrays with values for each cell. 118 * @example <script type="text/javascript"> 119 LABKEY.Utils.convertToTable( 120 { 121 fileName: 'output.csv', 122 rows: 123 [ 124 ['Row1Col1', 'Row1Col2'], 125 ['Row2Col1', 'Row2Col2'] 126 ], 127 delim: 'COMMA' 128 }); 129 </script> 130 */ 131 impl.convertToTable = function(config) { 132 formSubmit(LABKEY.ActionURL.buildURL("experiment", "convertArraysToTable"), config); 133 }; 134 135 /** 136 * Display an error dialog 137 * @param title 138 * @param msg 139 */ 140 impl.alert = function(title, msg) { 141 if (window.Ext4) { 142 Ext4.Msg.alert(title?Ext4.htmlEncode(title):"", msg?Ext4.htmlEncode(msg):"") 143 } 144 else if (window.Ext) { 145 Ext.Msg.alert(title?Ext.util.Format.htmlEncode(title):"", msg?Ext.util.Format.htmlEncode(msg):""); 146 } 147 else { 148 alert(LABKEY.Utils.encodeHtml(title + ' : ' + msg)); 149 } 150 }; 151 152 /** 153 * Provides a generic error callback. This helper show a modal dialog, log the error to the console 154 * and will log the error to the audit log table. The user must have insert permissions on the selected container for 155 * this to work. By default, it will insert the error into the Shared project. A containerPath param can be passed to 156 * use a different container. The intent of this helper is to provide site admins with a mechanism to identify errors associated 157 * with client-side code. If noAuditLog=true is used, the helper will not log the error. 158 * 159 * @param {Object} error The error object passed to the callback function 160 * @param {String} [error.containerPath] Container where errors will be logged. Defaults to /shared 161 * @param {Boolean} [error.noAuditLog] If false, the errors will not be logged in the audit table. Defaults to true 162 * 163 * @example <script type="text/javascript"> 164 //basic usage 165 LABKEY.Query.selectRows({ 166 schemaName: 'core', 167 queryName: 'users', 168 success: function(){}, 169 failure: LABKEY.Utils.onError 170 }); 171 172 //custom container and turning off logging 173 LABKEY.Query.selectRows({ 174 schemaName: 'core', 175 queryName: 'users', 176 success: function(){}, 177 failure: function(error){ 178 error.containerPath = 'myContainer'; 179 error.noAuditLog = true; 180 LABKEY.Utils.onError(error); 181 } 182 }); 183 </script> 184 */ 185 impl.onError = function(error) { 186 187 if (!error) 188 return; 189 190 console.log('ERROR: ' + error.exception); 191 console.log(error); 192 193 if (!error.noAuditLog) 194 { 195 LABKEY.Query.insertRows({ 196 //it would be nice to store them in the current folder, but we cant guarantee the user has write access.. 197 containerPath: error.containerPath || '/shared', 198 schemaName: 'auditlog', 199 queryName: 'Client API Actions', 200 rows: [{ 201 EventType: "Client API Actions", 202 Key1: 'Client Error', 203 //NOTE: labkey should automatically crop these strings to the allowable length for that field 204 Key2: window.location.href, 205 Key3: (error.stackTrace && LABKEY.Utils.isArray(error.stackTrace) ? error.stackTrace.join('\n') : null), 206 Comment: (error.exception || error.statusText || error.message), 207 Date: new Date() 208 }], 209 success: function() {}, 210 failure: function(error){ 211 console.log('Problem logging error'); 212 console.log(error); 213 } 214 }); 215 } 216 }; 217 218 /** 219 * Sets the title of the webpart on the page. This change is not sticky, so it will be reverted on refresh. 220 * @param {string} title The title string 221 * @param {integer} webPartId The ID of the webpart 222 */ 223 impl.setWebpartTitle = function(title, webPartId) 224 { 225 $('table#webpart_' + webPartId + ' span[class=labkey-wp-title-text]').html(LABKEY.Utils.encodeHtml(title)); 226 }; 227 228 /** 229 * Adds new listener to be executed when all required scripts are fully loaded. 230 * @param {Mixed} config Either a callback function, or an object with the following properties: 231 * 232 * <li>callback (required) A function that will be called when required scripts are loaded.</li> 233 * <li>scope (optional) The scope to be used for the callback function. Defaults to the current scope.</li> 234 * <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> 235 * @example <script type="text/javascript"> 236 //simple usage 237 LABKEY.onReady(function(){ 238 //your code here. will be executed once scripts have loaded 239 }); 240 241 // 242 LABKEY.Utils.onReady({ 243 scope: this, 244 scripts: ['/myModule/myScript.js', 'AnotherScript.js], 245 callback: function(){ 246 //your code here. will be executed once scripts have loaded 247 }); 248 }); 249 </script> 250 */ 251 impl.onReady = function(config) 252 { 253 var scope; 254 var callback; 255 var scripts; 256 257 if (LABKEY.Utils.isFunction(config)) 258 { 259 scope = this; 260 callback = config; 261 scripts = null; 262 } 263 else if (LABKEY.Utils.isObject(config) && LABKEY.Utils.isFunction(config.callback)) 264 { 265 scope = config.scope || this; 266 callback = config.callback; 267 scripts = config.scripts; 268 } 269 else 270 { 271 LABKEY.Utils.alert("Configuration Error", "Improper configuration for LABKEY.onReady()"); 272 return; 273 } 274 275 if (scripts) 276 { 277 LABKEY.requiresScript(scripts, callback, scope, true); 278 } 279 else 280 { 281 $(function() { callback.call(scope); }); 282 } 283 }; 284 285 impl.addClass = function(element, cls) 286 { 287 if (LABKEY.Utils.isDefined(element)) 288 { 289 if (LABKEY.Utils.isDefined(element.classList)) 290 { 291 element.classList.add(cls); 292 } 293 else 294 { 295 element.className += " " + cls; 296 } 297 } 298 }; 299 300 impl.removeClass = function(element, cls) 301 { 302 if (LABKEY.Utils.isDefined(element)) 303 { 304 if (LABKEY.Utils.isDefined(element.classList)) 305 { 306 element.classList.remove(cls); 307 } 308 else 309 { 310 // http://stackoverflow.com/questions/195951/change-an-elements-css-class-with-javascript 311 var reg = new RegExp("(?:^|\\s)" + cls + "(?!\\S)/g"); 312 element.className.replace(reg, ''); 313 } 314 } 315 }; 316 317 impl.replaceClass = function(element, removeCls, addCls) 318 { 319 LABKEY.Utils.removeClass(element, removeCls); 320 LABKEY.Utils.addClass(element, addCls); 321 }; 322 323 //private 324 impl.loadAjaxContent = function(response, targetEl, success, scope, useReplace) { 325 var json = LABKEY.Utils.decode(response.responseText); 326 if (!json) 327 return; 328 329 if (json.moduleContext) 330 LABKEY.applyModuleContext(json.moduleContext); 331 332 if (json.requiredCssScripts) 333 LABKEY.requiresCss(json.requiredCssScripts); 334 335 if (json.implicitCssIncludes) 336 { 337 for (var i=0;i<json.implicitCssIncludes.length;i++) 338 { 339 LABKEY.requestedCssFiles(json.implicitCssIncludes[i]); 340 } 341 } 342 343 if (json.requiredJsScripts && json.requiredJsScripts.length) 344 { 345 LABKEY.requiresScript(json.requiredJsScripts, onLoaded, this, true); 346 } 347 else 348 { 349 onLoaded(); 350 } 351 352 function onLoaded() 353 { 354 if (json.html) 355 { 356 if (LABKEY.Utils.isString(targetEl)) { 357 targetEl = $('#'+targetEl); 358 } 359 360 if (useReplace === true) { 361 targetEl.replaceWith(json.html); 362 } 363 else { 364 targetEl.html(json.html); // execute scripts...so bad 365 } 366 367 if (LABKEY.Utils.isFunction(success)) { 368 success.call(scope || window); 369 } 370 371 if (json.implicitJsIncludes) 372 LABKEY.loadedScripts(json.implicitJsIncludes); 373 } 374 } 375 }; 376 377 impl.tabInputHandler = function(elementSelector) { 378 // http://stackoverflow.com/questions/1738808/keypress-in-jquery-press-tab-inside-textarea-when-editing-an-existing-text 379 $(elementSelector).keydown(function (e) { 380 if (e.keyCode == 9) { 381 var myValue = "\t"; 382 var startPos = this.selectionStart; 383 var endPos = this.selectionEnd; 384 var scrollTop = this.scrollTop; 385 this.value = this.value.substring(0, startPos) + myValue + this.value.substring(endPos,this.value.length); 386 this.focus(); 387 this.selectionStart = startPos + myValue.length; 388 this.selectionEnd = startPos + myValue.length; 389 this.scrollTop = scrollTop; 390 391 e.preventDefault(); 392 } 393 }); 394 }; 395 396 impl.signalWebDriverTest = function(signalName, signalResult) 397 { 398 var signalContainerId = 'testSignals'; 399 var signalContainerSelector = '#' + signalContainerId; 400 var signalContainer = $(signalContainerSelector); 401 var formHTML = '<div id="' + signalContainerId + '"/>'; 402 403 if (!signalContainer.length) 404 { 405 $('body').append(formHTML); 406 signalContainer = $(signalContainerSelector); 407 signalContainer.hide(); 408 } 409 410 signalContainer.find('div[name="' + signalName + '"]').remove(); 411 signalContainer.append('<div name="' + signalName + '" id="' + LABKEY.Utils.id() + '"/>'); 412 if (signalResult) 413 { 414 signalContainer.find('div[name="' + signalName + '"]').attr("value", signalResult); 415 } 416 }; 417 418 return impl; 419 420 }(LABKEY.Utils, jQuery); 421