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