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