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