5745fad57404eaea33662a78d18fa26cb4c418fc
[Evergreen.git] / Open-ILS / web / js / dojo / openils / Util.js
1 /* ---------------------------------------------------------------------------
2  * Copyright (C) 2008  Georgia Public Library Service
3  * Bill Erickson <erickson@esilibrary.com>
4  *
5  * This program is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU General Public License
7  * as published by the Free Software Foundation; either version 2
8  * of the License, or (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  * ---------------------------------------------------------------------------
15  */
16
17
18 /**
19  * General purpose, static utility functions
20  */
21
22 if(!dojo._hasResource["openils.Util"]) {
23     dojo._hasResource["openils.Util"] = true;
24     dojo.provide("openils.Util");
25     dojo.require("dojo.date.locale");
26     dojo.require("dojo.date.stamp");
27     dojo.require('openils.Event');
28     dojo.declare('openils.Util', null, {});
29
30
31     openils.Util.timeStampRegexp =
32         /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[\+-]\d{2})(\d{2})$/;
33
34     openils.Util.timeStampAsDateObj = function(s) {
35         if (s.constructor.name == "Date") return s;
36         return dojo.date.stamp.fromISOString(
37             s.replace(openils.Util.timeStampRegexp, "$1:$2")
38         );
39     }
40
41     /**
42      * Returns a locale-appropriate representation of a timestamp when the
43      * timestamp (first argument) is actually a string as provided by
44      * fieldmapper objects.
45      * The second argument is an optional argument that will be provided
46      * as the second argument to dojo.date.locale.format()
47      */
48     openils.Util.timeStamp = function(s, opts) {
49         if (typeof(opts) == "undefined") opts = {};
50
51         return dojo.date.locale.format(
52             openils.Util.timeStampAsDateObj(s), opts
53         );
54     };
55
56     openils.Util._userFullNameFields = [
57         "prefix", "first_given_name", "second_given_name",
58         "family_name", "suffix", "alias", "usrname"
59     ];
60
61     /**
62      * Return an array of all the name-related attributes, with nulls replaced
63      * by empty strings, from a given actor.usr fieldmapper object, to be used
64      * as the arguments to any string formatting function that wants them.
65      * Code to do this is duplicated all over the place and should be
66      * simplified as we go.
67      */
68     openils.Util.userFullName = function(user) {
69         return dojo.map(
70             openils.Util._userFullNameFields,
71             function(a) { return user[a]() || ""; }
72         );
73     };
74
75     /**
76      * Same as openils.Util.userFullName, but with a hash of results instead
77      * of an array (dojo.string.substitute(), for example, can use this too).
78      */
79     openils.Util.userFullNameHash = function(user) {
80         var hash = {};
81         dojo.forEach(
82             openils.Util._userFullNameFields,
83             function(a) { hash[a] = user[a]() || ""; }
84         );
85         return hash;
86     };
87
88     /**
89      * Wrapper for dojo.addOnLoad that verifies a valid login session is active
90      * before adding the function to the onload set
91      */
92     openils.Util.addOnLoad = function(func, noSes) {
93         if(func) {
94             if(!noSes) {
95                 dojo.require('openils.User');
96                 if(!openils.User.authtoken) 
97                     return;
98             }
99             console.log("adding onload " + func.name);
100             dojo.addOnLoad(func);
101         }
102     };
103
104     /**
105      * Returns true if the provided array contains the specified value
106      */
107     openils.Util.arrayContains = function(arr, val) {
108         for(var i = 0; arr && i < arr.length; i++) {
109             if(arr[i] == val)
110                 return true;
111         }
112         return false;
113     };
114
115     /**
116      * Given a HTML select object, returns the currently selected value
117      */
118     openils.Util.selectorValue = function(sel) {
119         if(!sel) return null;
120         var idx = sel.selectedIndex;
121         if(idx < 0) return null;
122         var o = sel.options[idx];
123         var v = o.value; 
124         if(v == null) v = o.innerHTML;
125         return v;
126     }
127
128     /**
129      * Returns the character code of the provided (or current window) event
130      */
131     openils.Util.getCharCode = function(evt) {
132         evt = (evt) ? evt : ((window.event) ? event : null); 
133         if(evt) {
134             return (evt.charCode ? evt.charCode : 
135                 ((evt.which) ? evt.which : evt.keyCode ));
136         } else { return -1; }
137     }
138
139
140     /**
141      * Registers a handler for when the Enter key is pressed while 
142      * the provided DOM node has focus.
143      */
144     openils.Util.registerEnterHandler = function(domNode, func) {
145             if(!(domNode && func)) return;
146             domNode.onkeydown = function(evt) {
147             var code = openils.Util.getCharCode(evt);
148             if(code == 13 || code == 3) 
149                 func();
150         }
151         }
152
153
154     /**
155      * Parses opensrf response objects to see if they contain 
156      * data and/or an ILS event.  This only calls request.recv()
157      * once, so in a streaming context, it's necessary to loop on
158      * this method. 
159      * @param r The OpenSRF Request object
160      * @param eventOK If true, any found events will be returned as responses.  
161      * If false, they will be treated as error conditions and their content will
162      * be alerted if openils.Util.alertEvent is set to true.  Also, if eventOk is
163      * false, the response content will be null when an event is encountered.
164      * @param isList If true, assume the response will be a list of data and
165      * check the 1st item in the list for event-ness instead of the list itself.
166      */
167     openils.Util.alertEvent = true;
168     openils.Util.readResponse = function(r, eventOk, isList) {
169         var msg = r.recv();
170         if(msg == null) return msg;
171         var val = msg.content();
172         var testval = val;
173         if(isList && dojo.isArray(val))
174             testval = val[0];
175         if(e = openils.Event.parse(testval)) {
176             if(eventOk || e.textcode == 'SUCCESS') return e;
177             console.log(e.toString());
178
179             // session timed out.  Stop propagation of requests queued by Util.onload 
180             // and launch the XUL login dialog if possible
181             var retryLogin = false;
182             if(e.textcode == 'NO_SESSION') {
183                 openils.User.authtoken = null; 
184                 if(openils.XUL.isXUL()) {
185                     retryLogin = true;
186                     openils.XUL.getNewSession( function() { location.href = location.href } );
187                 } else {
188                     // TODO: make the oilsLoginDialog templated via dojo so it can be 
189                     // used as a standalone widget
190                 }
191             }
192
193             if(openils.Util.alertEvent && !retryLogin)
194                 alert(e);
195             return null;
196         }
197         return val;
198     };
199
200
201     /**
202      * Given a DOM node, adds the provided class to the node 
203      */
204     openils.Util.addCSSClass = function(node, cls) {
205         if(!(node && cls)) return; 
206         var className = node.className;
207
208         if(!className) {
209             node.className = cls;
210             return;
211         }
212
213         var classList = className.split(/\s+/);
214         var newName = '';
215             
216         for (var i = 0; i < classList.length; i++) {
217             if(classList[i] == cls) return;
218             if(classList[i] != null)
219                 newName += classList[i] + " ";
220         }
221
222         newName += cls;
223         node.className = newName;
224     },
225
226     /**
227      * Given a DOM node, removes the provided class from the CSS class 
228      * name list.
229      */
230     openils.Util.removeCSSClass = function(node, cls) {
231         if(!(node && cls && node.className)) return;
232         var classList = node.className.split(/\s+/);
233         var className = '';
234         for(var i = 0; i < classList.length; i++) {
235             if (typeof(cls) == "object") { /* assume regex */
236                 if (!cls.test(classList[i])) {
237                     if(i == 0)
238                         className = classList[i];
239                     else
240                         className += ' ' + classList[i];
241                 }
242             } else {
243                 if (classList[i] != cls) {
244                     if(i == 0)
245                         className = classList[i];
246                     else
247                         className += ' ' + classList[i];
248                 }
249             }
250         }
251         node.className = className;
252     }
253
254     openils.Util.objectSort = function(list, field) {
255         if(dojo.isArray(list)) {
256             if(!field) field = 'id';
257             return list.sort(
258                 function(a, b) {
259                     if(a[field]() > b[field]()) return 1;
260                     return -1;
261                 }
262             );
263         }
264         return [];
265     };
266
267     openils.Util.isTrue = function(val) {
268         return (val && val != '0' && !(val+'').match(/^f$/i));
269     };
270
271     /**
272      * Turns a list into a mapped object.
273      * @param list The list
274      * @param pkey The field to use as the map key 
275      * @param isFunc If true, the map key field is an accessor function 
276      * that will return the value of the map key
277      */
278     openils.Util.mapList = function(list, pkey, isFunc) {
279         if(!(list && pkey)) 
280             return null;
281         var map = {};
282         for(var i in list) {
283             if(isFunc)
284                 map[list[i][pkey]()] = list[i];
285             else
286                 map[list[i][pkey]] = list[i];
287         }
288         return map;
289     };
290
291     /**
292      * Convenience function to trim leading and trailing whitespace at once.
293      */
294     openils.Util.trimString = function(s) {
295         return s.replace(/^\s*(.+)?\s*$/,"$1");
296     }
297
298     /**
299      * Assume a space-separated interval string, with optional comma
300      * E.g. "1 year, 2 days"  "3 days 6 hours"
301      */
302     openils.Util.intervalToSeconds = function(interval) {
303         var d = new Date();
304         var start = d.getTime();
305         var parts = interval.split(' ');
306         for(var i = 0; i < parts.length; i += 2)  {
307             var type = parts[i+1].replace(/s?,?$/,'');
308             switch(type) {
309                 case 'mon': // postgres
310                     type = 'month'; // dojo
311                     break;
312                 // add more as necessary
313             }
314
315             d = dojo.date.add(d, type, Number(parts[i]));
316         }
317         return Number((d.getTime() - start) / 1000);
318     };
319
320     openils.Util.hide = function(node) {
321         if(typeof node == 'string')
322             node = dojo.byId(node);
323         dojo.style(node, 'display', 'none');
324         dojo.style(node, 'visibility', 'hidden');
325     };
326
327     openils.Util.show = function(node, displayType) {
328         if(typeof node == 'string')
329             node = dojo.byId(node);
330         displayType = displayType || 'block';
331         dojo.style(node, 'display', displayType);
332         dojo.style(node, 'visibility', 'visible');
333     };
334
335     /** Toggles the display using show/hide, depending on the current value for CSS 'display' */
336     openils.Util.toggle = function(node, displayType) {
337         if(typeof node == 'string')
338             node = dojo.byId(node);
339         if(dojo.style(node, 'display') == 'none')
340             openils.Util.show(node, displayType);
341         else
342             openils.Util.hide(node);
343     };
344
345     openils.Util.appendClear = function(node, child) {
346         if(typeof node == 'string')
347             node = dojo.byId(node);
348         while(node.childNodes[0])
349             node.removeChild(node.childNodes[0]);
350         node.appendChild(child);
351     };
352
353     /**
354      * Plays a sound file via URL.  Only works with browsers
355      * that support HTML 5 <audio> element.  E.g. Firefox 3.5
356      */
357     openils.Util.playAudioUrl = function(urlString) {
358         if(!urlString) return;
359         var audio = document.createElement('audio');
360         audio.setAttribute('src', urlString);
361         audio.setAttribute('autoplay', 'true');
362         document.body.appendChild(audio);
363         document.body.removeChild(audio);
364     }
365
366     /**
367      * Return the properties of an object as a list. Saves typing.
368      */
369     openils.Util.objectProperties = function(obj) {
370         var K = [];
371         for (var k in obj) K.push(k);
372         return K;
373     }
374
375     /**
376      * Return the values of an object as a list. There may be a Dojo
377      * idiom or something that makes this redundant. Check into that.
378      */
379     openils.Util.objectValues = function(obj) {
380         var V = [];
381         for (var k in obj) V.push(obj[k]);
382         return V;
383     }
384
385     openils.Util.uniqueElements = function(L) {
386         var o = {};
387         for (var k in L) o[L[k]] = true;
388         return openils.Util.objectProperties(o);
389     }
390
391     openils.Util.uniqueObjects = function(list, field) {
392         var sorted = openils.Util.objectSort(list, field);
393         var results = [];
394         for (var i = 0; i < sorted.length; i++) {
395             if (!i || (sorted[i][field]() != sorted[i-1][field]()))
396                 results.push(sorted[i]);
397         }
398         return results;
399     };
400
401     /**
402      * Highlight instances of each pattern in the given DOM node
403      * Inspired by the jquery plugin
404      * http://johannburkard.de/blog/programming/javascript/highlight-javascript-text-higlighting-jquery-plugin.html
405      */
406     openils.Util.hilightNode = function(node, patterns, args) {
407
408         args = args ||{};
409         var hclass = args.classname || 'oils-highlight';
410
411         function _hilightNode(node, pat) {
412
413             if(node.nodeType == 3) { 
414
415                 pat = pat.toUpperCase();
416                 var text = node.data.toUpperCase();
417                 var pos = -1;
418
419                 // find each instance of pat in the current node
420                 while( (pos =  text.indexOf(pat, pos + 1)) >= 0 ) {
421
422                     var wrapper = dojo.create('span', {className : hclass});
423                     var midnode = node.splitText(pos);
424                     midnode.splitText(pat.length);
425                     wrapper.appendChild(midnode.cloneNode(true));
426                     midnode.parentNode.replaceChild(wrapper, midnode);
427                 }
428
429             } else if(node.nodeType == 1 && node.childNodes[0]) {
430
431                 // not a text node?  have you checked the children?
432                 dojo.forEach(
433                     node.childNodes,
434                     function(child) { _hilightNode(child, pat); }
435                 );
436             }
437         }
438
439         // descend the tree for each pattern, since nodes are changed during highlighting
440         dojo.forEach(patterns, function(pat) { _hilightNode(node, pat); });
441     };
442
443     openils.Util._legacyModulePaths = {};
444     /*****
445      * Take the URL of a JS file and magically turn it into something that
446      * dojo.require can load by registering a module path for it ... and load it.
447      *****/
448     openils.Util.requireLegacy = function(url) {
449         var bURL = url.replace(/\/[^\/]+$/,'');
450         var file = url.replace(/^.*\/([^\/]+)$/,'$1');
451         var libname = url.replace(/^.*?\/(.+)\/[^\/]+$/,'$1').replace(/[^a-z]/ig,'_');
452         var modname = libname + '.' + file.replace(/\.js$/,'');
453
454         if (!openils.Util._legacyModulePaths[libname]) {
455             dojo.registerModulePath(libname,bURL);
456             openils.Util._legacyModulePaths[libname] = {};
457         }
458
459         if (!openils.Util._legacyModulePaths[libname][modname]) {
460             dojo.require(modname, true);
461             openils.Util._legacyModulePaths[libname][modname] = true;
462         }
463
464         return openils.Util._legacyModulePaths[libname][modname];
465     };
466
467     /**
468      * Takes a chunk of HTML, inserts it into a new window, prints the window, 
469      * then closes the windw.  To provide ample printer queueing time, automatically
470      * wait a short time before closing the window after calling .print().  The amount
471      * of time to wait is based on the size of the data to be printed.
472      * @param html The HTML string
473      * @param callback Optional post-printing callback
474      */
475     openils.Util.printHtmlString = function(html, callback) {
476
477         var win = window.open('', 'Print Window', 'resizable,width=800,height=600,scrollbars=1'); 
478
479         // force the new window to the background
480         win.blur(); 
481         window.focus(); 
482
483         win.document.body.innerHTML = html;
484         win.print();
485
486         setTimeout(
487             function() { 
488                 win.close();
489                 if(callback)
490                     callback();
491             },
492             // 1k == 1 second pause, max 10 seconds
493             Math.min(html.length, 10000)
494         );
495     };
496
497 }
498