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