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