96c21c5e2f82803bf015365ed42128409fd1e478
[infodrom/rico3] / ricoClient / js / rico.js
1 /*
2  *  (c) 2005-2009 Richard Cowin (http://openrico.org)
3  *  (c) 2005-2009 Matt Brown (http://dowdybrown.com)
4  *
5  *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
6  *  file except in compliance with the License. You may obtain a copy of the License at
7  *
8  *         http://www.apache.org/licenses/LICENSE-2.0
9  *
10  *  Unless required by applicable law or agreed to in writing, software distributed under the
11  *  License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
12  *  either express or implied. See the License for the specific language governing permissions
13  *  and limitations under the License.
14  */
15
16 /**
17  * @namespace Main Rico object
18  */
19 var Rico = {
20   Version: '3.0b1',
21   loadRequested: 1,
22   loadComplete: 2,
23   theme: {},
24
25   init : function() {
26     try {  // fix IE background image flicker (credit: www.mister-pixel.com)
27       document.execCommand("BackgroundImageCache", false, true);
28     } catch(err) {}
29     if (typeof Rico_CONFIG == 'object') {
30       this.setPaths(Rico_CONFIG.jsDir,Rico_CONFIG.cssDir,Rico_CONFIG.imgDir);
31       if (Rico_CONFIG.enableLogging) this.enableLogging();
32     }
33     this.preloadMsgs='';
34     this.baseHref= location.protocol + "//" + location.host;
35     this.loadedFiles={};
36     this.windowIsLoaded=false;
37     this.onLoadCallbacks=[];
38     var onloadAction=Rico.bind(this,'windowLoaded');
39     if (window.addEventListener)
40       window.addEventListener('load', onloadAction, false);
41     else if (window.attachEvent)
42       window.attachEvent('onload', onloadAction);
43     this.onLoad(function() { Rico.log('Pre-load messages:\n'+Rico.preloadMsgs); });
44   },
45
46   setPaths : function(jsDir,cssDir,imgDir,htmDir) {
47     this.jsDir = jsDir;
48     this.cssDir = cssDir;
49     this.imgDir = imgDir;
50     this.htmDir = htmDir || jsDir;
51   },
52
53   // Array entries can reference a javascript file or css stylesheet
54   // A dependency on another module can be indicated with a plus-sign prefix: '+DependsOnModule'
55   moduleDependencies : {
56     DragAndDrop: ['ricoDragDrop.js'],
57     Calendar   : ['ricoCalendar.js'],
58     SearchBox  : ['ricoSearch.js'],
59     Tree       : ['ricoTree.js'],
60     ColorPicker: ['ricoColorPicker.js'],
61     SimpleGrid : ['ricoGridCommon.js', 'ricoSimpleGrid.js'],
62     LiveGridBasic : ['ricoGridCommon.js', 'ricoLiveGrid.js'],
63     LiveGrid      : ['+LiveGridBasic', 'ricoLiveGridControls.js'],
64     LiveGridMenu  : ['ricoLiveGridMenu.js'],
65     LiveGridAjax  : ['+LiveGrid', 'ricoLiveGridAjax.js'],
66     LiveGridJSON  : ['+LiveGridAjax', 'ricoLiveGridJSON.js'],
67     LiveGridForms : ['+LiveGridAjax', '+LiveGridMenu', 'ricoLiveGridForms.js']
68   },
69
70   languages : {
71     de: "translations/ricoLocale_de.js",
72     en: "translations/ricoLocale_en.js",
73     es: "translations/ricoLocale_es.js",
74     fr: "translations/ricoLocale_fr.js",
75     it: "translations/ricoLocale_it.js",
76     ja: "translations/ricoLocale_ja.js",
77     ko: "translations/ricoLocale_ko.js",
78     pt: "translations/ricoLocale_pt.js",
79     zh: "translations/ricoLocale_zh.js"
80   },
81
82   languageInclude : function(lang2) {
83     var filename=this.languages[lang2];
84     if (filename) this.include(filename);
85     return !!filename;\r
86   },
87
88   acceptLanguage : function(acceptLang) {
89     var arLang=acceptLang.toLowerCase().split(',');\r
90     for (var i=0; i<arLang.length; i++) {\r
91       var lang2=arLang[i].match(/\w\w/);
92       if (!lang2) continue;\r
93       if (this.languageInclude(lang2)) return true;\r
94     }
95     return false;\r
96   },
97
98   /**
99    *  Expects one or more module or file names and loads
100    *  the appropriate files.
101    *  MUST call Rico.setPaths() before calling this method
102    */
103   loadModule : function() {
104     for (var a=0, length=arguments.length; a<length; a++) {
105       var name=arguments[a];
106       var dep=this.moduleDependencies[name];
107       if (dep) {
108         for (var i=0; i<dep.length; i++) {
109           if (dep[i].substring(0,1)=='+') {
110             this.loadModule(dep[i].slice(1));
111           } else {
112             this.include(dep[i]);
113           }
114         }
115       } else {
116         this.include(name);
117       }
118     }
119   },
120
121   include : function(filename) {
122     if (this.loadedFiles[filename]) return;
123     this.addPreloadMsg('include: '+filename);
124     var ext = filename.substr(filename.lastIndexOf('.')+1);
125     switch (ext.toLowerCase()) {
126       case 'js':
127         this.loadedFiles[filename]=filename.substring(0,4)=='rico' ? this.loadRequested : this.loadComplete;
128         var el = document.createElement('script');
129         el.type = 'text/javascript';
130         el.src = this.jsDir+filename;
131         document.getElementsByTagName('head')[0].appendChild(el);
132         break;
133       case 'css':
134         var el = document.createElement('link');
135         el.type = 'text/css';
136         el.rel = 'stylesheet';
137         el.href = this.cssDir+filename;
138         this.loadedFiles[filename]=this.loadComplete;
139         document.getElementsByTagName('head')[0].appendChild(el);
140         break;
141     }
142   },
143
144   // called after a script file has finished loading
145   includeLoaded: function(filename) {
146     this.loadedFiles[filename]=this.loadComplete;
147     this.checkIfComplete();
148   },
149
150   // called by the document onload event
151   windowLoaded: function() {
152     this.windowIsLoaded=true;
153     this.checkIfComplete();
154   },
155
156   checkIfComplete: function() {
157     var waitingFor=this.windowIsLoaded ? '' : 'window';
158     for(var filename in  this.loadedFiles) {
159       if (this.loadedFiles[filename]==this.loadRequested)
160         waitingFor+=' '+filename;
161     }
162     //window.status='waitingFor: '+waitingFor;
163     this.addPreloadMsg('waitingFor: '+waitingFor);
164     if (waitingFor.length==0) {
165       this.addPreloadMsg('Processing callbacks');
166       while (this.onLoadCallbacks.length > 0) {
167         var callback=this.onLoadCallbacks.shift();
168         if (callback) callback();
169       }
170     }
171   },
172
173   onLoad: function(callback,frontOfQ) {
174     if (frontOfQ)
175       this.onLoadCallbacks.unshift(callback);
176     else
177       this.onLoadCallbacks.push(callback);
178     this.checkIfComplete();
179   },
180
181   isKonqueror : navigator.userAgent.toLowerCase().indexOf("konqueror") > -1,
182   isIE:  !!(window.attachEvent && navigator.userAgent.indexOf('Opera') === -1),
183   isOpera: navigator.userAgent.indexOf('Opera') > -1,
184   isWebKit: navigator.userAgent.indexOf('AppleWebKit/') > -1,
185   isGecko:  navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('KHTML') === -1,
186   ieVersion: /MSIE (\d+\.\d+);/.test(navigator.userAgent) ? new Number(RegExp.$1) : null,
187
188   // logging funtions
189
190   startTime : new Date(),
191
192   timeStamp: function() {
193     var stamp = new Date();
194     return (stamp.getTime()-this.startTime.getTime())+": ";
195   },
196
197 setDebugArea: function(id, forceit) {
198   if (!this.debugArea || forceit) {
199     var newarea=document.getElementById(id);
200     if (!newarea) return;
201     this.debugArea=newarea;
202     newarea.value='';
203   }
204 },
205
206 addPreloadMsg: function(msg) {
207   this.preloadMsgs+=this.timeStamp()+msg+"\n";
208 },
209
210 log: function() {},
211
212 enableLogging: function() {
213   if (this.debugArea) {
214     this.log = function(msg, resetFlag) {
215       if (resetFlag) this.debugArea.value='';
216       this.debugArea.value+=this.timeStamp()+msg+"\n";
217     };
218   } else if (window.console) {
219     if (window.console.firebug)
220       this.log = function(msg) { window.console.log(this.timeStamp(),msg); };
221     else
222       this.log = function(msg) { window.console.log(this.timeStamp()+msg); };\r
223   } else if (window.opera) {
224     this.log = function(msg) { window.opera.postError(this.timeStamp()+msg); };
225   }
226 },
227
228 $: function(e) {
229   return typeof e == 'string' ? document.getElementById(e) : e;
230 },
231
232 bind: function() {
233   var binderArgs = Array.prototype.slice.call(arguments);
234   var object = binderArgs.shift();
235   var method = binderArgs.shift();
236   return function() {
237     var callerArgs = Array.prototype.slice.call(arguments);
238     return object[method].apply(object,binderArgs.concat(callerArgs));
239   };
240 },
241
242 runLater: function() {
243   var args = Array.prototype.slice.call(arguments);
244   var msec = args.shift();
245   var object = args.shift();
246   var method = args.shift();
247   return setTimeout(function() { object[method].apply(object,args); },msec);
248 },
249
250 visible: function(element) {
251   return Rico.getStyle(element,"display") != 'none';
252 },
253
254 show: function(element) {
255   element.style.display = '';
256 },
257
258 hide: function(element) {
259   element.style.display = 'none';
260 },
261
262 toggle: function(element) {
263   element.style.display = element.style.display == 'none' ? '' : 'none';
264 },
265
266 viewportOffset: function(element) {
267   var offset=Rico.cumulativeOffset(element);
268   offset.left -= this.docScrollLeft();
269   offset.top -= this.docScrollTop();
270   return offset;
271 },
272
273 /**
274  * Return text within an html element
275  * @param el DOM node
276  * @param xImg true to exclude img tag info
277  * @param xForm true to exclude input, select, and textarea tags
278  * @param xClass exclude elements with a class name of xClass
279  */
280 getInnerText: function(el,xImg,xForm,xClass) {
281   switch (typeof el) {
282     case 'string': return el;
283     case 'undefined': return el;
284     case 'number': return el.toString();
285   }
286   var cs = el.childNodes;
287   var l = cs.length;
288   var str = "";
289   for (var i = 0; i < l; i++) {
290     switch (cs[i].nodeType) {
291     case 1: //ELEMENT_NODE
292       if (this.getStyle(cs[i],'display')=='none') continue;
293       if (xClass && this.hasClass(cs[i],xClass)) continue;
294       switch (cs[i].tagName.toLowerCase()) {
295         case 'img':   if (!xImg) str += cs[i].alt || cs[i].title || cs[i].src; break;
296         case 'input': if (!xForm && !cs[i].disabled && cs[i].type.toLowerCase()=='text') str += cs[i].value; break;
297         case 'select': if (!xForm && cs[i].selectedIndex>=0) str += cs[i].options[cs[i].selectedIndex].text; break;
298         case 'textarea': if (!xForm && !cs[i].disabled) str += cs[i].value; break;
299         default:      str += this.getInnerText(cs[i],xImg,xForm,xClass); break;
300       }
301       break;
302     case 3: //TEXT_NODE
303       str += cs[i].nodeValue;
304       break;
305     }
306   }
307   return str;
308 },
309
310 /**
311  * Return value of a node in an XML response.
312  * For Konqueror 3.5, isEncoded must be true.
313  */
314 getContentAsString: function( parentNode, isEncoded ) {
315   if (isEncoded) return this._getEncodedContent(parentNode);
316   if (typeof parentNode.xml != 'undefined') return this._getContentAsStringIE(parentNode);
317   return this._getContentAsStringMozilla(parentNode);
318 },
319
320 _getEncodedContent: function(parentNode) {
321   if (parentNode.innerHTML) return parentNode.innerHTML;
322   switch (parentNode.childNodes.length) {
323     case 0:  return "";
324     case 1:  return parentNode.firstChild.nodeValue;
325     default: return parentNode.childNodes[1].nodeValue;
326   }
327 },
328
329 _getContentAsStringIE: function(parentNode) {
330   var contentStr = "";
331   for ( var i = 0 ; i < parentNode.childNodes.length ; i++ ) {
332      var n = parentNode.childNodes[i];
333      contentStr += (n.nodeType == 4) ? n.nodeValue : n.xml;
334   }
335   return contentStr;
336 },
337
338 _getContentAsStringMozilla: function(parentNode) {
339    var xmlSerializer = new XMLSerializer();
340    var contentStr = "";
341    for ( var i = 0 ; i < parentNode.childNodes.length ; i++ ) {
342         var n = parentNode.childNodes[i];
343         if (n.nodeType == 4) { // CDATA node
344             contentStr += n.nodeValue;
345         }
346         else {
347           contentStr += xmlSerializer.serializeToString(n);
348       }
349    }
350    return contentStr;
351 },
352
353 /**
354  * @param n a number (or a string to be converted using parseInt)
355  * @returns the integer value of n, or 0 if n is not a number
356  */
357 nan2zero: function(n) {
358   if (typeof(n)=='string') n=parseInt(n,10);
359   return isNaN(n) || typeof(n)=='undefined' ? 0 : n;
360 },
361
362 stripTags: function(s) {
363   return s.replace(/<\/?[^>]+>/gi, '');
364 },
365
366 truncate: function(s,length) {
367   return s.length > length ? s.substr(0, length - 3) + '...' : s;
368 },
369
370 zFill: function(n,slen, radix) {
371   var s=n.toString(radix || 10);
372   while (s.length<slen) s='0'+s;
373   return s;
374 },
375
376 keys: function(obj) {
377   var objkeys=[];
378   for(var k in obj)
379     objkeys.push[k];
380   return objkeys;
381 },
382
383 /**
384  * @param e event object
385  * @returns the key code stored in the event
386  */
387 eventKey: function(e) {
388   if( typeof( e.keyCode ) == 'number'  ) {
389     return e.keyCode; //DOM
390   } else if( typeof( e.which ) == 'number' ) {
391     return e.which;   //NS 4 compatible
392   } else if( typeof( e.charCode ) == 'number'  ) {
393     return e.charCode; //also NS 6+, Mozilla 0.9+
394   }
395   return -1;  //total failure, we have no way of obtaining the key code
396 },
397
398 eventLeftClick: function(e) {
399   return (((e.which) && (e.which == 1)) ||
400           ((e.button) && (e.button == 1)));
401 },
402
403 eventRelatedTarget: function(e) {
404   return e.relatedTarget;
405 },
406
407   /**
408  * Return the previous sibling that has the specified tagName
409  */
410  getPreviosSiblingByTagName: function(el,tagName) {
411         var sib=el.previousSibling;
412         while (sib) {
413                 if ((sib.tagName==tagName) && (sib.style.display!='none')) return sib;
414                 sib=sib.previousSibling;
415         }
416         return null;
417  },
418
419 /**
420  * Return the parent of el that has the specified tagName.
421  * @param el DOM node
422  * @param tagName tag to search for
423  * @param className optional
424  */
425 getParentByTagName: function(el,tagName,className) {
426   var par=el;
427   tagName=tagName.toLowerCase();
428   while (par) {
429     if (par.tagName && par.tagName.toLowerCase()==tagName) {
430       if (!className || par.className.indexOf(className)>=0) return par;
431     }
432         par=par.parentNode;
433   }
434   return null;
435 },
436
437 /**
438  * Wrap the children of a DOM element in a new element
439  * @param el the element whose children are to be wrapped
440  * @param cls class name of the wrapper (optional)
441  * @param id id of the wrapper (optional)
442  * @param wrapperTag type of wrapper element to be created (optional, defaults to DIV)
443  * @returns new wrapper element
444  */
445 wrapChildren: function(el,cls,id,wrapperTag) {
446   var wrapper = document.createElement(wrapperTag || 'div');
447   if (id) wrapper.id=id;
448   if (cls) wrapper.className=cls;
449   while (el.firstChild) {
450     wrapper.appendChild(el.firstChild);
451   }
452   el.appendChild(wrapper);
453   return wrapper;
454 },
455
456 /**
457  * Positions ctl over icon
458  * @param ctl (div with position:absolute)
459  * @param icon element (img, button, etc) that ctl should be displayed next to
460  */
461 positionCtlOverIcon: function(ctl,icon) {
462   icon=this.$(icon);
463   var offsets=this.cumulativeOffset(icon);
464   var scrTop=this.docScrollTop();
465   var winHt=this.windowHeight();
466   if (ctl.style.display=='none') ctl.style.display='block';
467   //var correction=this.isIE ? 1 : 2;  // based on a 1px border
468   var correction=2;  // based on a 1px border
469   var lpad=this.nan2zero(this.getStyle(icon,'paddingLeft'));
470   ctl.style.left = (offsets.left+lpad+correction)+'px';
471   var newTop=offsets.top + correction;// + scrTop;
472   var ctlht=ctl.offsetHeight;
473   var iconht=icon.offsetHeight;
474   var margin=10;  // account for shadow
475   if (newTop+iconht+ctlht+margin < winHt+scrTop) {
476     newTop+=iconht;  // display below icon
477   } else {
478     newTop=Math.max(newTop-ctlht,scrTop);  // display above icon
479   }
480   ctl.style.top = newTop+'px';
481 },
482
483 /**
484  * Creates a form element
485  * @param parent new element will be appended to this node
486  * @param elemTag element to be created (input, button, select, textarea, ...)
487  * @param elemType for input tag this specifies the type (checkbox, radio, text, ...)
488  * @param id id for new element
489  * @param name name for new element, if not specified then name will be the same as the id
490  * @returns new element
491  */
492 createFormField: function(parent,elemTag,elemType,id,name) {
493   var field;
494   if (typeof name!='string') name=id;
495   if (this.isIE) {
496     // IE cannot set NAME attribute on dynamically created elements
497     var s=elemTag+' id="'+id+'"';
498     if (elemType) {
499       s+=' type="'+elemType+'"';
500     }
501     if (elemTag.match(/^(form|input|select|textarea|object|button|img)$/)) {
502       s+=' name="'+name+'"';
503     }
504     field=document.createElement('<'+s+' />');
505   } else {
506     field=document.createElement(elemTag);
507     if (elemType) {
508       field.type=elemType;
509     }
510     field.id=id;
511     if (typeof field.name=='string') {
512       field.name=name;
513     }
514   }
515   parent.appendChild(field);
516   return field;
517 },
518
519 /**
520  * Adds a new option to the end of a select list
521  * @returns new option element
522  */
523 addSelectOption: function(elem,value,text) {
524   var opt=document.createElement('option');
525   if (typeof value=='string') opt.value=value;
526   opt.text=text;
527   if (this.isIE) {
528     elem.add(opt);
529   } else {
530     elem.add(opt,null);
531   }
532   return opt;
533 },
534
535 /**
536  * @returns the value of the specified cookie (or null if it doesn't exist)
537  */
538 getCookie: function(itemName) {
539   var arg = itemName+'=';
540   var alen = arg.length;
541   var clen = document.cookie.length;
542   var i = 0;
543   while (i < clen) {
544     var j = i + alen;
545     if (document.cookie.substring(i, j) == arg) {
546       var endstr = document.cookie.indexOf (';', j);
547       if (endstr == -1) {
548         endstr=document.cookie.length;
549       }
550       return unescape(document.cookie.substring(j, endstr));
551     }
552     i = document.cookie.indexOf(' ', i) + 1;
553     if (i == 0) break;
554   }
555   return null;
556 },
557
558 getTBody: function(tab) {
559   return tab.tBodies.length==0 ? tab.appendChild(document.createElement("tbody")) : tab.tBodies[0];
560 },
561
562 /**
563  * Write information to a cookie.
564  * For cookies to be retained for the current session only, set daysToKeep=null.
565  * To erase a cookie, pass a negative daysToKeep value.
566  * @see <a href="http://www.quirksmode.org/js/cookies.html">Quirksmode article</a> for more information about cookies.
567  */
568 setCookie: function(itemName,itemValue,daysToKeep,cookiePath,cookieDomain) {
569         var c = itemName+"="+escape(itemValue);
570         if (typeof(daysToKeep)=='number') {
571                 var date = new Date();
572                 date.setTime(date.getTime()+(daysToKeep*24*60*60*1000));
573                 c+="; expires="+date.toGMTString();
574         }
575         if (typeof(cookiePath)=='string') {
576     c+="; path="+cookiePath;
577   }
578         if (typeof(cookieDomain)=='string') {
579     c+="; domain="+cookieDomain;
580   }
581   document.cookie = c;
582 },
583
584 phrasesById : {},
585 /** thousands separator for number formatting */
586 thouSep : ",",
587 /** decimal point for number formatting */
588 decPoint: ".",
589 /** target language (2 character code) */
590 langCode: "en",
591 /** date format */
592 dateFmt : "mm/dd/yyyy",
593 /** time format */
594 timeFmt : "hh:nn:ss a/pm",
595 /** month name array (Jan is at index 0) */
596 monthNames: ['January','February','March','April','May','June',
597              'July','August','September','October','November','December'],
598 /** day of week array (Sunday is at index 0) */
599 dayNames: ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'],
600
601 /**
602  * @param monthIdx 0-11
603  * @returns month abbreviation
604  */
605 monthAbbr: function(monthIdx) {
606   return this.monthNamesShort ? this.monthNamesShort[monthIdx] : this.monthNames[monthIdx].substr(0,3);
607 },
608
609 /**
610  * @param dayIdx 0-6 (Sunday=0)
611  * @returns day of week abbreviation
612  */
613 dayAbbr: function(dayIdx) {
614   return this.dayNamesShort ? this.dayNamesShort[dayIdx] : this.dayNames[dayIdx].substr(0,3);
615 },
616
617 addPhraseId: function(phraseId, phrase) {
618   this.phrasesById[phraseId]=phrase;
619 },
620
621 getPhraseById: function(phraseId) {
622   var phrase=this.phrasesById[phraseId];
623   if (!phrase) {
624     alert('Error: missing phrase for '+phraseId);
625     return '';
626   }
627   if (arguments.length <= 1) return phrase;
628   var a=arguments;
629   return phrase.replace(/(\$\d)/g,
630     function($1) {
631       var idx=parseInt($1.charAt(1),10);
632       return (idx < a.length) ? a[idx] : '';
633     }
634   );
635 },
636
637 /**
638  * Format a positive number (integer or float)
639  * @param posnum number to format
640  * @param decPlaces the number of digits to display after the decimal point
641  * @param thouSep the character to use as the thousands separator
642  * @param decPoint the character to use as the decimal point
643  * @returns formatted string
644  */
645 formatPosNumber: function(posnum,decPlaces,thouSep,decPoint) {
646   var a=posnum.toFixed(decPlaces).split(/\./);
647   if (thouSep) {
648     var rgx = /(\d+)(\d{3})/;
649     while (rgx.test(a[0])) {
650       a[0]=a[0].replace(rgx, '$1'+thouSep+'$2');
651     }
652   }
653   return a.join(decPoint);
654 },
655
656 /**
657  * Format a number according to the specs in fmt object.
658  * @returns string, wrapped in a span element with a class of: negNumber, zeroNumber, posNumber
659  * These classes can be set in CSS to display negative numbers in red, for example.
660  *
661  * @param n number to be formatted
662  * @param fmt may contain any of the following:<dl>
663  *   <dt>multiplier </dt><dd> the original number is multiplied by this amount before formatting</dd>
664  *   <dt>decPlaces  </dt><dd> number of digits to the right of the decimal point</dd>
665  *   <dt>decPoint   </dt><dd> character to be used as the decimal point</dd>
666  *   <dt>thouSep    </dt><dd> character to use as the thousands separator</dd>
667  *   <dt>prefix     </dt><dd> string added to the beginning of the result (e.g. a currency symbol)</dd>
668  *   <dt>suffix     </dt><dd> string added to the end of the result (e.g. % symbol)</dd>
669  *   <dt>negSign    </dt><dd> specifies format for negative numbers: L=leading minus, T=trailing minus, P=parens</dd>
670  *</dl>
671  */
672 formatNumber : function(n,fmt) {
673   if (typeof n=='string') n=parseFloat(n.replace(/,/,'.'),10);
674   if (isNaN(n)) return 'NaN';
675   if (typeof fmt.multiplier=='number') n*=fmt.multiplier;
676   var decPlaces=typeof fmt.decPlaces=='number' ? fmt.decPlaces : 0;
677   var thouSep=typeof fmt.thouSep=='string' ? fmt.thouSep : this.thouSep;
678   var decPoint=typeof fmt.decPoint=='string' ? fmt.decPoint : this.decPoint;
679   var prefix=fmt.prefix || "";
680   var suffix=fmt.suffix || "";
681   var negSign=typeof fmt.negSign=='string' ? fmt.negSign : "L";
682   negSign=negSign.toUpperCase();
683   var s,cls;
684   if (n<0.0) {
685     s=this.formatPosNumber(-n,decPlaces,thouSep,decPoint);
686     if (negSign=="P") s="("+s+")";
687     s=prefix+s;
688     if (negSign=="L") s="-"+s;
689     if (negSign=="T") s+="-";
690     cls='negNumber';
691   } else {
692     cls=n==0.0 ? 'zeroNumber' : 'posNumber';
693     s=prefix+this.formatPosNumber(n,decPlaces,thouSep,decPoint);
694   }
695   return "<span class='"+cls+"'>"+s+suffix+"</span>";
696 },
697
698 /**
699  * Converts a date to a string according to specs in fmt
700  * @returns formatted string
701  * @param d date to be formatted
702  * @param fmt string specifying the output format, may be one of the following:<dl>
703  * <dt>locale or localeDateTime</dt>
704  *   <dd>use javascript's built-in toLocaleString() function</dd>
705  * <dt>localeDate</dt>
706  *   <dd>use javascript's built-in toLocaleDateString() function</dd>
707  * <dt>translate or translateDateTime</dt>
708  *   <dd>use the formats specified in the Rico.dateFmt and Rico.timeFmt properties</dd>
709  * <dt>translateDate</dt>
710  *   <dd>use the date format specified in the Rico.dateFmt property</dd>
711  * <dt>Otherwise</dt>
712  *   <dd>Any combination of: yyyy, yy, mmmm, mmm, mm, m, dddd, ddd, dd, d, hh, h, HH, H, nn, ss, a/p</dd>
713  *</dl>
714  */
715 formatDate : function(d,fmt) {
716   var datefmt=(typeof fmt=='string') ? fmt : 'translateDate';
717   switch (datefmt) {
718     case 'locale':
719     case 'localeDateTime':
720       return d.toLocaleString();
721     case 'localeDate':
722       return d.toLocaleDateString();
723     case 'translate':
724     case 'translateDateTime':
725       datefmt=this.dateFmt+' '+this.timeFmt;
726       break;
727     case 'translateDate':
728       datefmt=this.dateFmt;
729       break;
730   }
731   return datefmt.replace(/(yyyy|yy|mmmm|mmm|mm|dddd|ddd|dd|hh|nn|ss|a\/p)/gi,
732     function($1) {
733       var h;
734       switch ($1) {
735       case 'yyyy': return d.getFullYear();
736       case 'yy':   return d.getFullYear().toString().substr(2);
737       case 'mmmm': return Rico.monthNames[d.getMonth()];
738       case 'mmm':  return Rico.monthAbbr(d.getMonth());
739       case 'mm':   return Rico.zFill(d.getMonth() + 1, 2);
740       case 'm':    return (d.getMonth() + 1);
741       case 'dddd': return Rico.dayNames[d.getDay()];
742       case 'ddd':  return Rico.dayAbbr(d.getDay());
743       case 'dd':   return Rico.zFill(d.getDate(), 2);
744       case 'd':    return d.getDate();
745       case 'hh':   return Rico.zFill((h = d.getHours() % 12) ? h : 12, 2);
746       case 'h':    return ((h = d.getHours() % 12) ? h : 12);
747       case 'HH':   return Rico.zFill(d.getHours(), 2);
748       case 'H':    return d.getHours();
749       case 'nn':   return Rico.zFill(d.getMinutes(), 2);
750       case 'ss':   return Rico.zFill(d.getSeconds(), 2);
751       case 'a/p':  return d.getHours() < 12 ? 'a' : 'p';
752       }
753     }
754   );
755 },
756
757 /**
758  * Converts a string in ISO 8601 format to a date object.
759  * @returns date object, or false if string is not a valid date or date-time.
760  * @param string value to be converted
761  * @param offset can be used to bias the conversion and must be in minutes if provided
762  * @see Based on <a href='http://delete.me.uk/2005/03/iso8601.html'>delete.me.uk article</a>
763  */
764 setISO8601 : function (string,offset) {
765   if (!string) return false;
766   var d = string.match(/(\d\d\d\d)(?:-?(\d\d)(?:-?(\d\d)(?:[T ](\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(Z|(?:([-+])(\d\d)(?::?(\d\d))?)?)?)?)?)?/);
767   if (!d) return false;
768   if (!offset) offset=0;
769   var date = new Date(d[1], 0, 1);
770
771   if (d[2]) { date.setMonth(d[2] - 1); }
772   if (d[3]) { date.setDate(d[3]); }
773   if (d[4]) { date.setHours(d[4]); }
774   if (d[5]) { date.setMinutes(d[5]); }
775   if (d[6]) { date.setSeconds(d[6]); }
776   if (d[7]) { date.setMilliseconds(Number("0." + d[7]) * 1000); }
777   if (d[8]) {
778       if (d[10] && d[11]) {
779         offset = (Number(d[10]) * 60) + Number(d[11]);
780       }
781       offset *= ((d[9] == '-') ? 1 : -1);
782       offset -= date.getTimezoneOffset();
783   }
784   var time = (Number(date) + (offset * 60 * 1000));
785   date.setTime(Number(time));
786   return date;
787 },
788
789 /**
790  * Convert date to an ISO 8601 formatted string.
791  * @param date date object to be converted
792  * @param format an integer in the range 1-6 (default is 6):<dl>
793  * <dt>1 (year)</dt>
794  *   <dd>YYYY (eg 1997)</dd>
795  * <dt>2 (year and month)</dt>
796  *   <dd>YYYY-MM (eg 1997-07)</dd>
797  * <dt>3 (complete date)</dt>
798  *   <dd>YYYY-MM-DD (eg 1997-07-16)</dd>
799  * <dt>4 (complete date plus hours and minutes)</dt>
800  *   <dd>YYYY-MM-DDThh:mmTZD (eg 1997-07-16T19:20+01:00)</dd>
801  * <dt>5 (complete date plus hours, minutes and seconds)</dt>
802  *   <dd>YYYY-MM-DDThh:mm:ssTZD (eg 1997-07-16T19:20:30+01:00)</dd>
803  * <dt>6 (complete date plus hours, minutes, seconds and a decimal
804  *   fraction of a second)</dt>
805  *   <dd>YYYY-MM-DDThh:mm:ss.sTZD (eg 1997-07-16T19:20:30.45+01:00)</dd>
806  *</dl>
807  * @see Based on: <a href='http://www.codeproject.com/jscript/dateformat.asp'>codeproject.com article</a>
808  */
809 toISO8601String : function (date, format, offset) {
810   if (!format) format=6;
811   if (!offset) {
812       offset = 'Z';
813   } else {
814       var d = offset.match(/([-+])([0-9]{2}):([0-9]{2})/);
815       var offsetnum = (Number(d[2]) * 60) + Number(d[3]);
816       offsetnum *= ((d[1] == '-') ? -1 : 1);
817       date = new Date(Number(Number(date) + (offsetnum * 60000)));
818   }
819
820   var zeropad = function (num) { return ((num < 10) ? '0' : '') + num; };
821
822   var str = date.getUTCFullYear();
823   if (format > 1) { str += "-" + zeropad(date.getUTCMonth() + 1); }
824   if (format > 2) { str += "-" + zeropad(date.getUTCDate()); }
825   if (format > 3) {
826       str += "T" + zeropad(date.getUTCHours()) +
827              ":" + zeropad(date.getUTCMinutes());
828   }
829   if (format > 5) {
830     var secs = Number(date.getUTCSeconds() + "." +
831                ((date.getUTCMilliseconds() < 100) ? '0' : '') +
832                zeropad(date.getUTCMilliseconds()));
833     str += ":" + zeropad(secs);
834   } else if (format > 4) {
835     str += ":" + zeropad(date.getUTCSeconds());
836   }
837
838   if (format > 3) { str += offset; }
839   return str;
840 },
841
842 /**
843  * Returns a new XML document object
844  */
845 createXmlDocument : function() {
846   if (document.implementation && document.implementation.createDocument) {
847     var doc = document.implementation.createDocument("", "", null);
848     // some older versions of Moz did not support the readyState property
849     // and the onreadystate event so we patch it! 
850     if (doc.readyState == null) {
851       doc.readyState = 1;
852       doc.addEventListener("load", function () {
853         doc.readyState = 4;
854         if (typeof doc.onreadystatechange == "function") {
855           doc.onreadystatechange();
856         }
857       }, false);
858     }
859     return doc;
860   }
861
862   if (window.ActiveXObject)
863       return Rico.tryFunctions(
864         function() { return new ActiveXObject('MSXML2.DomDocument');   },
865         function() { return new ActiveXObject('Microsoft.DomDocument');},
866         function() { return new ActiveXObject('MSXML.DomDocument');    },
867         function() { return new ActiveXObject('MSXML3.DomDocument');   }
868       ) || false;
869   return null;
870 }
871
872 };
873
874 /**
875  * Update the contents of an HTML element via an AJAX call
876  */
877 Rico.ajaxUpdater = function(elem,url,options) {
878   this.updateSend(elem,url,options);
879 };
880
881 Rico.ajaxUpdater.prototype = {
882   updateSend : function(elem,url,options) {
883     this.element=elem;
884     this.onComplete=options.onComplete;
885     options.onComplete=Rico.bind(this,'updateComplete');
886     new Rico.ajaxRequest(url,options);
887   },
888
889   updateComplete : function(xhr) {
890     this.element.innerHTML=xhr.responseText;
891     if (this.onComplete) this.onComplete(xhr);
892   }
893 };
894
895 Rico.writeDebugMsg=Rico.log;  // for backwards compatibility
896
897 Rico.init();