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-2019 LabKey Corporation 5 * <p/> 6 * Licensed under the Apache License, Version 2.0 (the "License"); 7 * you may not use this file except in compliance with the License. 8 * You may obtain a copy of the License at 9 * <p/> 10 * http://www.apache.org/licenses/LICENSE-2.0 11 * <p/> 12 * Unless required by applicable law or agreed to in writing, software 13 * distributed under the License is distributed on an "AS IS" BASIS, 14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 * See the License for the specific language governing permissions and 16 * limitations under the License. 17 * <p/> 18 */ 19 20 LABKEY.Utils = new function(impl, $) { 21 22 // Insert a hidden html FORM into to page, put the form values into it, and submit it - the server's response will 23 // make the browser pop up a dialog 24 var formSubmit = function(url, formData) 25 { 26 if (!formData) 27 formData = {}; 28 if (!formData['X-LABKEY-CSRF']) 29 formData['X-LABKEY-CSRF'] = LABKEY.CSRF; 30 31 var formId = LABKEY.Utils.generateUUID(); 32 33 var html = []; 34 html.push('<f'); // avoid form tag, it causes skipfish false positive 35 html.push('orm method="POST" id="' + formId + '"action="' + url + '">'); 36 for (var name in formData) 37 { 38 if (!formData.hasOwnProperty(name)) 39 continue; 40 41 var value = formData[name]; 42 if (value === undefined) 43 continue; 44 45 html.push( '<input type="hidden"' + 46 ' name="' + LABKEY.Utils.encodeHtml(name) + '"' + 47 ' value="' + LABKEY.Utils.encodeHtml(value) + '" />'); 48 } 49 html.push("</form>"); 50 51 $('body').append(html.join('')); 52 $('form#' + formId).submit(); 53 }; 54 55 var displayModalAlert = function(title, msg) { 56 displayModal(title, msg, undefined, true); 57 }; 58 59 var displayModal = function(title, msg, fn, args, disableBackdrop) { 60 var modal = $('#lk-utils-modal'); 61 62 if (modal.length === 0) { 63 $('body').append([ 64 '<div id="lk-utils-modal" class="modal fade" role="dialog">', 65 '<div class="modal-dialog"><div class="modal-content"></div></div>', 66 '</div>' 67 ].join('')); 68 69 modal = $('#lk-utils-modal'); 70 } 71 var html = [ 72 '<div class="modal-header">', 73 '<button type="button" class="close" data-dismiss="modal">×</button>', 74 '<h4 class="modal-title">' + LABKEY.Utils.encodeHtml(title) + '</h4>', 75 '</div>', 76 '<div class="modal-body">' 77 ]; 78 if (msg) { 79 html.push('<br><p>' + LABKEY.Utils.encodeHtml(msg) + '<br></p>'); 80 } 81 html.push( 82 '<div id="modal-fn-body"></div>', 83 '</div>' 84 ); 85 86 modal.find('.modal-content').html(html.join('')); 87 if (fn && typeof fn === 'function') { 88 fn.apply(this, args); 89 } 90 91 // prevent the modal from being closed by clicking outside the dialog 92 if (disableBackdrop) { 93 modal.modal({backdrop: 'static'}); 94 } 95 96 modal.modal('show'); 97 }; 98 99 /** 100 * Documentation available in core/Utils.js -- search for "@name displayAjaxErrorResponse" 101 */ 102 impl.displayAjaxErrorResponse = function(responseObj, exceptionObj, showExceptionClass, msgPrefix) 103 { 104 if (responseObj.status == 0) 105 { 106 // Don't show an error dialog if the user cancelled the request in the browser, like navigating 107 // to another page 108 return; 109 } 110 111 var error = LABKEY.Utils.getMsgFromError(responseObj, exceptionObj, { 112 msgPrefix: msgPrefix, 113 showExceptionClass: showExceptionClass 114 }); 115 LABKEY.Utils.alert("Error", error); 116 }; 117 118 /** 119 * Documentation available in core/Utils.js -- search for "@name convertToExcel" 120 */ 121 impl.convertToExcel = function(spreadsheet) { 122 var formData = { 'json': JSON.stringify(spreadsheet) }; 123 formSubmit(LABKEY.ActionURL.buildURL("experiment", "convertArraysToExcel"), formData); 124 }; 125 126 /** 127 * Documentation available in core/Utils.js -- search for "@name convertToTable" 128 */ 129 impl.convertToTable = function(config) { 130 var formData = { 'json': JSON.stringify(config) }; 131 formSubmit(LABKEY.ActionURL.buildURL("experiment", "convertArraysToTable"), formData); 132 }; 133 134 /** 135 * Documentation available in core/Util.js -- search for "@name postToAction" 136 */ 137 impl.postToAction = function (href, formData) { 138 formSubmit(href, formData); 139 }; 140 141 /** 142 * Documentation available in core/Util.js -- search for "@name confirmAndPost" 143 */ 144 impl.confirmAndPost = function (message, href, formData) { 145 if (confirm(message)) 146 formSubmit(href, formData); 147 148 return false; 149 }; 150 151 /** 152 * Documentation specified in core/Utils.js -- search for "@name alert" 153 */ 154 impl.alert = function(title, msg) { 155 if (window.Ext4) { 156 Ext4.Msg.alert(title?Ext4.htmlEncode(title):"", msg?Ext4.htmlEncode(msg):"") 157 } 158 else if (window.Ext) { 159 Ext.Msg.alert(title?Ext.util.Format.htmlEncode(title):"", msg?Ext.util.Format.htmlEncode(msg):""); 160 } 161 else { 162 displayModalAlert(title, msg); 163 } 164 }; 165 166 /** 167 * Documentation specified in core/Utils.js -- search for "@name modal" 168 */ 169 impl.modal = function(title, msg, fn, args, disableBackdrop) { 170 displayModal(title, msg, fn, args, disableBackdrop); 171 }; 172 173 /** 174 * Documentation specified in core/Utils.js -- search for "@name onError" 175 */ 176 impl.onError = function(error) { 177 178 if (!error) 179 return; 180 181 console.log('ERROR: ' + error.exception); 182 console.log(error); 183 184 if (!error.noAuditLog) 185 { 186 LABKEY.Query.insertRows({ 187 //it would be nice to store them in the current folder, but we cant guarantee the user has write access.. 188 containerPath: error.containerPath || '/shared', 189 schemaName: 'auditlog', 190 queryName: 'Client API Actions', 191 rows: [{ 192 EventType: "Client API Actions", 193 Key1: 'Client Error', 194 //NOTE: labkey should automatically crop these strings to the allowable length for that field 195 Key2: window.location.href, 196 Key3: (error.stackTrace && LABKEY.Utils.isArray(error.stackTrace) ? error.stackTrace.join('\n') : null), 197 Comment: (error.exception || error.statusText || error.message), 198 Date: new Date() 199 }], 200 success: function() {}, 201 failure: function(error){ 202 console.log('Problem logging error'); 203 console.log(error); 204 } 205 }); 206 } 207 }; 208 209 /** 210 * Documentation specified in core/Utils.js -- search for "@name setWebpartTitle" 211 */ 212 impl.setWebpartTitle = function(title, webPartId) 213 { 214 $('#webpart_' + webPartId + ' span.labkey-wp-title-text').html(LABKEY.Utils.encodeHtml(title)); 215 }; 216 217 /** 218 * Documentation specified in core/Utils.js -- search for "@name onReady" 219 */ 220 impl.onReady = function(config) 221 { 222 var scope; 223 var callback; 224 var scripts; 225 226 if (LABKEY.Utils.isFunction(config)) 227 { 228 scope = this; 229 callback = config; 230 scripts = null; 231 } 232 else if (LABKEY.Utils.isObject(config) && LABKEY.Utils.isFunction(config.callback)) 233 { 234 scope = config.scope || this; 235 callback = config.callback; 236 scripts = config.scripts; 237 } 238 else 239 { 240 LABKEY.Utils.alert("Configuration Error", "Improper configuration for LABKEY.onReady()"); 241 return; 242 } 243 244 if (scripts) 245 { 246 LABKEY.requiresScript(scripts, callback, scope, true); 247 } 248 else 249 { 250 $(function() { callback.call(scope); }); 251 } 252 }; 253 254 impl.addClass = function(element, cls) 255 { 256 if (LABKEY.Utils.isDefined(element)) 257 { 258 if (LABKEY.Utils.isDefined(element.classList)) 259 { 260 element.classList.add(cls); 261 } 262 else 263 { 264 element.className += " " + cls; 265 } 266 } 267 }; 268 269 impl.removeClass = function(element, cls) 270 { 271 if (LABKEY.Utils.isDefined(element)) 272 { 273 if (LABKEY.Utils.isDefined(element.classList)) 274 { 275 element.classList.remove(cls); 276 } 277 else 278 { 279 // http://stackoverflow.com/questions/195951/change-an-elements-css-class-with-javascript 280 var reg = new RegExp("(?:^|\\s)" + cls + "(?!\\S)/g"); 281 element.className.replace(reg, ''); 282 } 283 } 284 }; 285 286 impl.replaceClass = function(element, removeCls, addCls) 287 { 288 LABKEY.Utils.removeClass(element, removeCls); 289 LABKEY.Utils.addClass(element, addCls); 290 }; 291 292 //private 293 impl.loadAjaxContent = function(response, targetEl, success, scope, useReplace) { 294 var json = LABKEY.Utils.decode(response.responseText); 295 if (!json) 296 return; 297 298 if (json.moduleContext) 299 LABKEY.applyModuleContext(json.moduleContext); 300 301 if (json.requiredCssScripts) 302 LABKEY.requiresCss(json.requiredCssScripts); 303 304 if (json.implicitCssIncludes) 305 { 306 for (var i=0;i<json.implicitCssIncludes.length;i++) 307 { 308 LABKEY.requestedCssFiles(json.implicitCssIncludes[i]); 309 } 310 } 311 312 if (json.requiredJsScripts && json.requiredJsScripts.length) 313 { 314 LABKEY.requiresScript(json.requiredJsScripts, onLoaded, this, true); 315 } 316 else 317 { 318 onLoaded(); 319 } 320 321 function onLoaded() 322 { 323 if (json.html) 324 { 325 if (LABKEY.Utils.isString(targetEl)) { 326 targetEl = $('#'+targetEl); 327 } 328 329 if (useReplace === true) { 330 targetEl.replaceWith(json.html); 331 } 332 else { 333 targetEl.html(json.html); // execute scripts...so bad 334 } 335 336 if (LABKEY.Utils.isFunction(success)) { 337 success.call(scope || window); 338 } 339 340 if (json.implicitJsIncludes) 341 LABKEY.loadedScripts(json.implicitJsIncludes); 342 } 343 } 344 }; 345 346 impl.tabInputHandler = function(elementSelector) { 347 // http://stackoverflow.com/questions/1738808/keypress-in-jquery-press-tab-inside-textarea-when-editing-an-existing-text 348 $(elementSelector).keydown(function (e) { 349 if (e.keyCode == 9) { 350 var myValue = "\t"; 351 var startPos = this.selectionStart; 352 var endPos = this.selectionEnd; 353 var scrollTop = this.scrollTop; 354 this.value = this.value.substring(0, startPos) + myValue + this.value.substring(endPos,this.value.length); 355 this.focus(); 356 this.selectionStart = startPos + myValue.length; 357 this.selectionEnd = startPos + myValue.length; 358 this.scrollTop = scrollTop; 359 360 e.preventDefault(); 361 } 362 }); 363 }; 364 365 /** 366 * Event handler that can be attached to text areas to let them handle indent/outdent with TAB/SHIFT-TAB. 367 * Handles region selection for multi-line indenting as well. 368 * Note that this overrides the browser's standard focus traversal keystrokes. 369 * Based off of postings from http://ajaxian.com/archives/handling-tabs-in-textareas 370 * @param event a KeyboardEvent or an Ext.EventObject for the keydown event 371 * 372 * @example 373 * Ext.EventManager.on('queryText', 'keydown', LABKEY.Utils.handleTabsInTextArea); 374 * @example 375 * textareaEl.addEventListener('keydown', LABKEY.Utils.handleTabsInTextArea); 376 */ 377 impl.handleTabsInTextArea = function(event) { 378 // unwrap the browser native event from Ext event object 379 event = event.browserEvent || event; 380 381 // Check if the user hit TAB or SHIFT-TAB 382 if (event.key === 'Tab' && !event.ctrlKey && !event.altKey) 383 { 384 var t = event.target; 385 386 // IE supports createRange 387 if (document.selection && document.selection.createRange) 388 { 389 var range = document.selection.createRange(); 390 var stored_range = range.duplicate(); 391 stored_range.moveToElementText(t); 392 stored_range.setEndPoint('EndToEnd', range); 393 t.selectionStart = stored_range.text.length - range.text.length; 394 t.selectionEnd = t.selectionStart + range.text.length; 395 t.setSelectionRange = function(start, end) 396 { 397 var range = this.createTextRange(); 398 range.collapse(true); 399 range.moveStart("character", start); 400 range.moveEnd("character", end - start); 401 range.select(); 402 }; 403 } 404 405 var ss = t.selectionStart; 406 var se = t.selectionEnd; 407 var newSelectionStart = ss; 408 var scrollTop = t.scrollTop; 409 410 if (ss !== se) 411 { 412 // In case selection was not the entire line (e.g. selection begins in the middle of a line) 413 // we need to tab at the beginning as well as at the start of every following line. 414 var pre = t.value.slice(0,ss); 415 var sel = t.value.slice(ss,se); 416 var post = t.value.slice(se,t.value.length); 417 418 // If our selection starts in the middle of the line, include the full line 419 if (pre.length > 0 && pre.lastIndexOf('\n') !== pre.length - 1) 420 { 421 // Add the beginning of the line to the indented area 422 sel = pre.slice(pre.lastIndexOf('\n') + 1, pre.length).concat(sel); 423 // Remove it from the prefix 424 pre = pre.slice(0, pre.lastIndexOf('\n') + 1); 425 if (!event.shiftKey) 426 { 427 // Add one to the starting index since we're going to add a tab before it 428 newSelectionStart++; 429 } 430 } 431 // If our last selected character is a new line, don't add a tab after it since that's 432 // part of the next line 433 if (sel.lastIndexOf('\n') === sel.length - 1) 434 { 435 sel = sel.slice(0, sel.length - 1); 436 post = '\n' + post; 437 } 438 439 // Shift means remove indentation 440 if (event.shiftKey) 441 { 442 // Remove one tab after each newline 443 sel = sel.replace(/\n\t/g,"\n"); 444 if (sel.indexOf('\t') === 0) 445 { 446 // Remove one leading tab, if present 447 sel = sel.slice(1, sel.length); 448 // We're stripping out a tab before the selection, so march it back one character 449 newSelectionStart--; 450 } 451 } 452 else 453 { 454 pre = pre.concat('\t'); 455 sel = sel.replace(/\n/g,"\n\t"); 456 } 457 458 var originalLength = t.value.length; 459 t.value = pre.concat(sel).concat(post); 460 t.setSelectionRange(newSelectionStart, se + (t.value.length - originalLength)); 461 } 462 // No text is selected 463 else 464 { 465 // Shift means remove indentation 466 if (event.shiftKey) 467 { 468 // Figure out where the current line starts 469 var lineStart = t.value.slice(0, ss).lastIndexOf('\n'); 470 if (lineStart < 0) 471 { 472 lineStart = 0; 473 } 474 // Look for the first tab 475 var tabIndex = t.value.slice(lineStart, ss).indexOf('\t'); 476 if (tabIndex !== -1) 477 { 478 // The line has a tab - need to remove it 479 tabIndex += lineStart; 480 t.value = t.value.slice(0, tabIndex).concat(t.value.slice(tabIndex + 1, t.value.length)); 481 if (ss === se) 482 { 483 ss--; 484 se = ss; 485 } 486 else 487 { 488 ss--; 489 se--; 490 } 491 } 492 } 493 else 494 { 495 // Shove a tab in at the cursor 496 t.value = t.value.slice(0,ss).concat('\t').concat(t.value.slice(ss,t.value.length)); 497 if (ss == se) 498 { 499 ss++; 500 se = ss; 501 } 502 else 503 { 504 ss++; 505 se++; 506 } 507 } 508 t.setSelectionRange(ss, se); 509 } 510 t.scrollTop = scrollTop; 511 512 // Don't let the browser treat it as a focus traversal 513 event.preventDefault(); 514 } 515 }; 516 517 impl.signalWebDriverTest = function(signalName, signalResult) 518 { 519 var signalContainerId = 'testSignals'; 520 var signalContainerSelector = '#' + signalContainerId; 521 var signalContainer = $(signalContainerSelector); 522 var formHTML = '<div id="' + signalContainerId + '"/>'; 523 524 if (!signalContainer.length) 525 { 526 $('body').append(formHTML); 527 signalContainer = $(signalContainerSelector); 528 signalContainer.hide(); 529 } 530 531 signalContainer.find('div[name=' + LABKEY.Utils.encode(signalName) + ']').remove(); 532 signalContainer.append('<div name="' + LABKEY.Utils.encodeHtml(signalName) + '" id="' + LABKEY.Utils.id() + '"/>'); 533 if (signalResult !== undefined) 534 { 535 signalContainer.find('div[name="' + LABKEY.Utils.encodeHtml(signalName) + '"]').attr("value", LABKEY.Utils.encodeHtml(signalResult)); 536 } 537 }; 538 539 /** 540 * Returns a string containing an absolute URL to a specific labkey.org documentation page. Modeled after HelpTopic.java getHelpTopicHref(). 541 * <li>topic (required) The documentation page name</li> 542 */ 543 impl.getHelpTopicHref = function(topic) 544 { 545 return LABKEY.helpLinkPrefix + topic; 546 }; 547 548 /** 549 * Returns a string containing a well-formed html anchor that opens a link to a specific labkey.org documentation 550 * page in a separate tab, using the standard target name. Modeled after HelpTopic.java getSimpleLinkHtml(). 551 * <li>topic (required) The documentation page name</li> 552 * <li>displayText (required) The text to display inside the anchor</li> 553 */ 554 impl.getSimpleLinkHtml = function(topic, displayText) 555 { 556 return '<a href="' + LABKEY.Utils.encodeHtml(LABKEY.Utils.getHelpTopicHref(topic)) + '" target="labkeyHelp">' + LABKEY.Utils.encodeHtml(displayText) + "</a>"; 557 }; 558 559 return impl; 560 561 }(LABKEY.Utils || new function() { return {}; }, jQuery); 562