Rename mask parameter into source
[misc/kostenrechnung] / lib / rico / ricoCommon.js
1 /*
2  *  Copyright 2005 Sabre Airline Solutions
3  *
4  *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
5  *  file except in compliance with the License. You may obtain a copy of the License at
6  *
7  *         http://www.apache.org/licenses/LICENSE-2.0
8  *
9  *  Unless required by applicable law or agreed to in writing, software distributed under the
10  *  License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
11  *  either express or implied. See the License for the specific language governing permissions
12  *  and limitations under the License.
13  */
14
15 if (typeof Rico=='undefined') throw("Cannot find the Rico object");
16 if (typeof Prototype=='undefined') throw("Rico requires the Prototype JavaScript framework");
17 Rico.prototypeVersion = parseFloat(Prototype.Version.split(".")[0] + "." + Prototype.Version.split(".")[1]);
18 if (Rico.prototypeVersion < 1.3) throw("Rico requires Prototype JavaScript framework version 1.3 or greater");
19
20 /** @namespace */
21 var RicoUtil = {
22
23 /**
24  * Finds all immediate children of e with tagName
25  * @param e DOM node or node id
26  * @param tagName tag name to search for (case-insensative)
27  * @returns array of matching elements
28  */
29 getDirectChildrenByTag: function(e, tagName) {
30   tagName=tagName.toLowerCase();
31   return $(e).childElements().inject([],function(result,child) {
32     if (child.tagName && child.tagName.toLowerCase()==tagName) result.push(child);
33     return result;});
34 },
35
36 /**
37  * Returns a new XML document object
38  */
39 createXmlDocument : function() {
40   if (document.implementation && document.implementation.createDocument) {
41     var doc = document.implementation.createDocument("", "", null);
42     // some older versions of Moz did not support the readyState property
43     // and the onreadystate event so we patch it! 
44     if (doc.readyState == null) {
45       doc.readyState = 1;
46       doc.addEventListener("load", function () {
47         doc.readyState = 4;
48         if (typeof doc.onreadystatechange == "function") {
49           doc.onreadystatechange();
50         }
51       }, false);
52     }
53
54      return doc;
55   }
56
57   if (window.ActiveXObject)
58       return Try.these(
59         function() { return new ActiveXObject('MSXML2.DomDocument');   },
60         function() { return new ActiveXObject('Microsoft.DomDocument');},
61         function() { return new ActiveXObject('MSXML.DomDocument');    },
62         function() { return new ActiveXObject('MSXML3.DomDocument');   }
63       ) || false;
64
65   return null;
66 },
67
68 /**
69  * Return text within an html element
70  * @param el DOM node
71  * @param xImg true to exclude img tag info
72  * @param xForm true to exclude input, select, and textarea tags
73  * @param xClass exclude elements with a class name of xClass
74  */
75 getInnerText: function(el,xImg,xForm,xClass) {
76   switch (typeof el) {
77     case 'string': return el;
78     case 'undefined': return el;
79     case 'number': return el.toString();
80   }
81   var cs = el.childNodes;
82   var l = cs.length;
83   var str = "";
84   for (var i = 0; i < l; i++) {
85    switch (cs[i].nodeType) {
86      case 1: //ELEMENT_NODE
87        if (Element.getStyle(cs[i],'display')=='none') continue;
88        if (xClass && Element.hasClassName(cs[i],xClass)) continue;
89        switch (cs[i].tagName.toLowerCase()) {
90          case 'img':   if (!xImg) str += cs[i].alt || cs[i].title || cs[i].src; break;
91          case 'input': if (cs[i].type=='hidden') continue;
92          case 'select':
93          case 'textarea': if (!xForm) str += $F(cs[i]) || ''; break;
94          default:      str += this.getInnerText(cs[i],xImg,xForm,xClass); break;
95        }
96        break;
97      case 3: //TEXT_NODE
98        str += cs[i].nodeValue;
99        break;
100    }
101   }
102   return str;
103 },
104
105 /**
106  * Return value of a node in an XML response.
107  * For Konqueror 3.5, isEncoded must be true.
108  */
109 getContentAsString: function( parentNode, isEncoded ) {
110   if (isEncoded) return this._getEncodedContent(parentNode);
111   if (typeof parentNode.xml != 'undefined') return this._getContentAsStringIE(parentNode);
112   return this._getContentAsStringMozilla(parentNode);
113 },
114
115 _getEncodedContent: function(parentNode) {
116   if (parentNode.innerHTML) return parentNode.innerHTML;
117   switch (parentNode.childNodes.length) {
118     case 0:  return "";
119     case 1:  return parentNode.firstChild.nodeValue;
120     default: return parentNode.childNodes[1].nodeValue;
121   }
122 },
123
124 _getContentAsStringIE: function(parentNode) {
125   var contentStr = "";
126   for ( var i = 0 ; i < parentNode.childNodes.length ; i++ ) {
127      var n = parentNode.childNodes[i];
128      contentStr += (n.nodeType == 4) ? n.nodeValue : n.xml;
129   }
130   return contentStr;
131 },
132
133 _getContentAsStringMozilla: function(parentNode) {
134    var xmlSerializer = new XMLSerializer();
135    var contentStr = "";
136    for ( var i = 0 ; i < parentNode.childNodes.length ; i++ ) {
137         var n = parentNode.childNodes[i];
138         if (n.nodeType == 4) { // CDATA node
139             contentStr += n.nodeValue;
140         }
141         else {
142           contentStr += xmlSerializer.serializeToString(n);
143       }
144    }
145    return contentStr;
146 },
147
148 /**
149  * @deprecated Will be removed in Rico 3
150  */
151 docElement: function() {
152   return (document.compatMode && document.compatMode.indexOf("CSS")!=-1) ? document.documentElement : document.getElementsByTagName("body")[0];
153 },
154
155 /**
156  * @returns available height, excluding scrollbar & margin
157  * @deprecated Use Prototype's document.viewport.getHeight instead
158  */
159 windowHeight: function() {
160   if (document.viewport) {
161     // use prototype 1.6 function
162     return document.viewport.getHeight();
163   } else {
164     return window.innerHeight? window.innerHeight : this.docElement().clientHeight;
165   }
166 },
167
168 /**
169  * @returns available width, excluding scrollbar & margin
170  * @deprecated Use Prototype's document.viewport.getWidth instead
171  */
172 windowWidth: function() {
173   if (document.viewport) {
174     // use prototype 1.6 function
175     return document.viewport.getWidth();
176   } else {
177     return this.docElement().clientWidth;
178   }
179 },
180
181 /**
182  * @deprecated Use Prototype's document.viewport.getScrollOffsets instead
183  */
184 docScrollLeft: function() {
185   if ( window.pageXOffset ) {
186     return window.pageXOffset;
187   } else if ( document.documentElement && document.documentElement.scrollLeft ) {
188     return document.documentElement.scrollLeft;
189   } else if ( document.body ) {
190     return document.body.scrollLeft;
191   } else {
192     return 0;
193   }
194 },
195
196 /**
197  * @deprecated Use Prototype's document.viewport.getScrollOffsets instead
198  */
199 docScrollTop: function() {
200   if ( window.pageYOffset ) {
201     return window.pageYOffset;
202   } else if ( document.documentElement && document.documentElement.scrollTop ) {
203     return document.documentElement.scrollTop;
204   } else if ( document.body ) {
205     return document.body.scrollTop;
206   } else {
207     return 0;
208   }
209 },
210
211 /**
212  * @param n a number (or a string to be converted using parseInt)
213  * @returns the integer value of n, or 0 if n is not a number
214  */
215 nan2zero: function(n) {
216   if (typeof(n)=='string') n=parseInt(n,10);
217   return isNaN(n) || typeof(n)=='undefined' ? 0 : n;
218 },
219
220 /**
221  * @param e event object
222  * @returns the key code stored in the event
223  */
224 eventKey: function(e) {
225   if( typeof( e.keyCode ) == 'number'  ) {
226     return e.keyCode; //DOM
227   } else if( typeof( e.which ) == 'number' ) {
228     return e.which;   //NS 4 compatible
229   } else if( typeof( e.charCode ) == 'number'  ) {
230     return e.charCode; //also NS 6+, Mozilla 0.9+
231   }
232   return -1;  //total failure, we have no way of obtaining the key code
233 },
234
235 /**
236  * Return the previous sibling that has the specified tagName
237  */
238  getPreviosSiblingByTagName: function(el,tagName) {
239         var sib=el.previousSibling;
240         while (sib) {
241                 if ((sib.tagName==tagName) && (sib.style.display!='none')) return sib;
242                 sib=sib.previousSibling;
243         }
244         return null;
245  },
246
247 /**
248  * Return the parent of el that has the specified tagName.
249  * @param el DOM node
250  * @param tagName tag to search for
251  * @param className optional
252  */
253 getParentByTagName: function(el,tagName,className) {
254   var par=el;
255   tagName=tagName.toLowerCase();
256   while (par) {
257     if (par.tagName && par.tagName.toLowerCase()==tagName) {
258       if (!className || par.className.indexOf(className)>=0) return par;
259     }
260         par=par.parentNode;
261   }
262   return null;
263 },
264
265 /**
266  * Wrap the children of a DOM element in a new element
267  * @param el the element whose children are to be wrapped
268  * @param cls class name of the wrapper (optional)
269  * @param id id of the wrapper (optional)
270  * @param wrapperTag type of wrapper element to be created (optional, defaults to DIV)
271  * @returns new wrapper element
272  */
273 wrapChildren: function(el,cls,id,wrapperTag) {
274   var wrapper = document.createElement(wrapperTag || 'div');
275   if (id) wrapper.id=id;
276   if (cls) wrapper.className=cls;
277   while (el.firstChild) {
278     wrapper.appendChild(el.firstChild);
279   }
280   el.appendChild(wrapper);
281   return wrapper;
282 },
283
284 /**
285  * Format a positive number (integer or float)
286  * @param posnum number to format
287  * @param decPlaces the number of digits to display after the decimal point
288  * @param thouSep the character to use as the thousands separator
289  * @param decPoint the character to use as the decimal point
290  * @returns formatted string
291  */
292 formatPosNumber: function(posnum,decPlaces,thouSep,decPoint) {
293   var a=posnum.toFixed(decPlaces).split(/\./);
294   if (thouSep) {
295     var rgx = /(\d+)(\d{3})/;
296     while (rgx.test(a[0])) {
297       a[0]=a[0].replace(rgx, '$1'+thouSep+'$2');
298     }
299   }
300   return a.join(decPoint);
301 },
302
303 /**
304  * Post condition - if childNodes[n] is refChild, than childNodes[n+1] is newChild.
305  * @deprecated Use Prototype's Element#insert instead
306  */
307 DOMNode_insertAfter: function(newChild,refChild) {
308   var parentx=refChild.parentNode;
309   if (parentx.lastChild==refChild) {
310     return parentx.appendChild(newChild);
311   } else {
312     return parentx.insertBefore(newChild,refChild.nextSibling);
313   }
314 },
315
316 /**
317  * Positions ctl over icon
318  * @param ctl (div with position:absolute)
319  * @param icon element (img, button, etc) that ctl should be displayed next to
320  */
321 positionCtlOverIcon: function(ctl,icon) {
322   var offsets=Position.page(icon);
323   var scrTop=this.docScrollTop();
324   var winHt=this.windowHeight();
325   if (ctl.style.display=='none') ctl.style.display='block';
326   var correction=Prototype.Browser.IE ? 1 : 2;  // based on a 1px border
327   var lpad=this.nan2zero(Element.getStyle(icon,'padding-left'));
328   ctl.style.left = (offsets[0]+lpad+correction)+'px';
329   var newTop=offsets[1] + correction + scrTop;
330   var ctlht=ctl.offsetHeight;
331   var iconht=icon.offsetHeight;
332   var margin=10;  // account for shadow
333   if (newTop+iconht+ctlht+margin < winHt+scrTop) {
334     newTop+=iconht;  // display below icon
335   } else {
336     newTop=Math.max(newTop-ctlht,scrTop);  // display above icon
337   }
338   ctl.style.top = newTop+'px';
339 },
340
341 /**
342  * Creates a form element 
343  * @param parent new element will be appended to this node
344  * @param elemTag element to be created (input, button, select, textarea, ...)
345  * @param elemType for input tag this specifies the type (checkbox, radio, text, ...)
346  * @param id id for new element
347  * @param name name for new element, if not specified then name will be the same as the id
348  * @returns new element
349  */
350 createFormField: function(parent,elemTag,elemType,id,name) {
351   var field;
352   if (typeof name!='string') name=id;
353   if (Prototype.Browser.IE) {
354     // IE cannot set NAME attribute on dynamically created elements
355     var s=elemTag+' id="'+id+'"';
356     if (elemType) {
357       s+=' type="'+elemType+'"';
358     }
359     if (elemTag.match(/^(form|input|select|textarea|object|button|img)$/)) {
360       s+=' name="'+name+'"';
361     }
362     field=document.createElement('<'+s+' />');
363   } else {
364     field=document.createElement(elemTag);
365     if (elemType) {
366       field.type=elemType;
367     }
368     field.id=id;
369     if (typeof field.name=='string') {
370       field.name=name;
371     }
372   }
373   parent.appendChild(field);
374   return field;
375 },
376
377 /**
378  * Adds a new option to the end of a select list
379  * @returns new option element
380  */
381 addSelectOption: function(elem,value,text) {
382   var opt=document.createElement('option');
383   if (typeof value=='string') opt.value=value;
384   opt.text=text;
385   if (Prototype.Browser.IE) {
386     elem.add(opt);
387   } else {
388     elem.add(opt,null);
389   }
390   return opt;
391 },
392
393 /**
394  * @returns the value of the specified cookie (or null if it doesn't exist)
395  */
396 getCookie: function(itemName) {
397   var arg = itemName+'=';
398   var alen = arg.length;
399   var clen = document.cookie.length;
400   var i = 0;
401   while (i < clen) {
402     var j = i + alen;
403     if (document.cookie.substring(i, j) == arg) {
404       var endstr = document.cookie.indexOf (';', j);
405       if (endstr == -1) {
406         endstr=document.cookie.length;
407       }
408       return unescape(document.cookie.substring(j, endstr));
409     }
410     i = document.cookie.indexOf(' ', i) + 1;
411     if (i == 0) break;
412   }
413   return null;
414 },
415
416 /**
417  * Write information to a cookie.
418  * For cookies to be retained for the current session only, set daysToKeep=null.
419  * To erase a cookie, pass a negative daysToKeep value.
420  * @see <a href="http://www.quirksmode.org/js/cookies.html">Quirksmode article</a> for more information about cookies.
421  */
422 setCookie: function(itemName,itemValue,daysToKeep,cookiePath,cookieDomain) {
423         var c = itemName+"="+escape(itemValue);
424         if (typeof(daysToKeep)=='number') {
425                 var date = new Date();
426                 date.setTime(date.getTime()+(daysToKeep*24*60*60*1000));
427                 c+="; expires="+date.toGMTString();
428         }
429         if (typeof(cookiePath)=='string') {
430     c+="; path="+cookiePath;
431   }
432         if (typeof(cookieDomain)=='string') {
433     c+="; domain="+cookieDomain;
434   }
435   document.cookie = c;
436 }
437
438 };
439
440
441 if (!RicoTranslate) {
442
443 /** @namespace Translation helper object. Values are set by loading one of the ricoLocale_xx.js files. */
444 var RicoTranslate = {
445   phrases : {},
446   phrasesById : {},
447   /** thousands separator for number formatting */
448   thouSep : ",",
449   /** decimal point for number formatting */
450   decPoint: ".",
451   /** target language (2 character code) */
452   langCode: "en",
453   re      : /^(\W*)\b(.*)\b(\W*)$/,
454   /** date format */
455   dateFmt : "mm/dd/yyyy",
456   /** time format */
457   timeFmt : "hh:nn:ss a/pm",
458   /** month name array (Jan is at index 0) */
459   monthNames: ['January','February','March','April','May','June',
460                'July','August','September','October','November','December'],
461   /** day of week array (Sunday is at index 0) */
462   dayNames: ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'],
463
464   /** 
465    * @param monthIdx 0-11
466    * @returns 3 character abbreviation
467    */
468   monthAbbr: function(monthIdx) {
469     return this.monthNames[monthIdx].substr(0,3);
470   },
471
472   /** 
473    * @param dayIdx 0-6 (Sunday=0)
474    * @returns 3 character day of week abbreviation
475    */
476   dayAbbr: function(dayIdx) {
477     return this.dayNames[dayIdx].substr(0,3);
478   },
479
480 /**
481  * @deprecated Use addPhraseId instead
482  */
483   addPhrase: function(fromPhrase, toPhrase) {
484     this.phrases[fromPhrase]=toPhrase;
485   },
486
487 /**
488  * @deprecated Use getPhraseById instead
489  * @param fromPhrase may contain multiple words/phrases separated by tabs
490  * and each portion will be looked up separately.
491  * Punctuation & spaces at the beginning or
492  * ending of a phrase are ignored.
493  */
494   getPhrase: function(fromPhrase) {
495     var words=fromPhrase.split(/\t/);
496     var transWord,translated = '';
497     for (var i=0; i<words.length; i++) {
498       if (this.re.exec(words[i])) {
499         transWord=this.phrases[RegExp.$2];
500         translated += (typeof transWord=='string') ? RegExp.$1+transWord+RegExp.$3 : words[i];
501       } else {
502         translated += words[i];
503       }
504     }
505     return translated;
506   },
507   
508   addPhraseId: function(phraseId, phrase) {
509     this.phrasesById[phraseId]=phrase;
510   },
511
512   getPhraseById: function(phraseId) {
513     var phrase=this.phrasesById[phraseId];
514     if (!phrase) {
515       alert('Error: missing phrase for '+phraseId);
516       return '';
517     }
518     if (arguments.length <= 1) return phrase;
519     var a=arguments;
520     return phrase.replace(/(\$\d)/g,
521       function($1) {
522         var idx=parseInt($1.charAt(1),10);
523         return (idx < a.length) ? a[idx] : '';
524       }
525     );
526   }
527 };
528
529 }
530
531
532 if (!Date.prototype.formatDate) {
533 /**
534  * Converts a date to a string according to specs in fmt
535  * @returns formatted string
536  * @param fmt string specifying the output format, may be one of the following:<dl>
537  * <dt>locale or localeDateTime</dt>
538  *   <dd>use javascript's built-in toLocaleString() function</dd>
539  * <dt>localeDate</dt>
540  *   <dd>use javascript's built-in toLocaleDateString() function</dd>
541  * <dt>translate or translateDateTime</dt>
542  *   <dd>use the date and time format specified in the RicoTranslate object</dd>
543  * <dt>translateDate</dt>
544  *   <dd>use the date format specified in the RicoTranslate object</dd>
545  * <dt>Otherwise</dt>
546  *   <dd>Any combination of: yyyy, yy, mmmm, mmm, mm, m, hh, h, HH, H, nn, ss, a/p</dd>
547  *</dl>
548  */
549   Date.prototype.formatDate = function(fmt) {
550     var d=this;
551     var datefmt=(typeof fmt=='string') ? fmt : 'translateDate';
552     switch (datefmt) {
553       case 'locale':
554       case 'localeDateTime':
555         return d.toLocaleString();
556       case 'localeDate':
557         return d.toLocaleDateString();
558       case 'translate':
559       case 'translateDateTime':
560         datefmt=RicoTranslate.dateFmt+' '+RicoTranslate.timeFmt;
561         break;
562       case 'translateDate':
563         datefmt=RicoTranslate.dateFmt;
564         break;
565     }
566     return datefmt.replace(/(yyyy|yy|mmmm|mmm|mm|dddd|ddd|dd|hh|nn|ss|a\/p)/gi,
567       function($1) {
568         var h;
569         switch ($1) {
570         case 'yyyy': return d.getFullYear();
571         case 'yy':   return d.getFullYear().toString().substr(2);
572         case 'mmmm': return RicoTranslate.monthNames[d.getMonth()];
573         case 'mmm':  return RicoTranslate.monthAbbr(d.getMonth());
574         case 'mm':   return (d.getMonth() + 1).toPaddedString(2);
575         case 'm':    return (d.getMonth() + 1);
576         case 'dddd': return RicoTranslate.dayNames[d.getDay()];
577         case 'ddd':  return RicoTranslate.dayAbbr(d.getDay());
578         case 'dd':   return d.getDate().toPaddedString(2);
579         case 'd':    return d.getDate();
580         case 'hh':   return ((h = d.getHours() % 12) ? h : 12).toPaddedString(2);
581         case 'h':    return ((h = d.getHours() % 12) ? h : 12);
582         case 'HH':   return d.getHours().toPaddedString(2);
583         case 'H':    return d.getHours();
584         case 'nn':   return d.getMinutes().toPaddedString(2);
585         case 'ss':   return d.getSeconds().toPaddedString(2);
586         case 'a/p':  return d.getHours() < 12 ? 'a' : 'p';
587         }
588       }
589     );
590   };
591 }
592
593 if (!Date.prototype.setISO8601) {
594 /**
595  * Converts a string in ISO 8601 format to a date object.
596  * @returns true if string is a valid date or date-time.
597  * @param string value to be converted
598  * @param offset can be used to bias the conversion and must be in minutes if provided
599  * @see Based on <a href='http://delete.me.uk/2005/03/iso8601.html'>delete.me.uk article</a>
600  */
601   Date.prototype.setISO8601 = function (string,offset) {
602     if (!string) return false;
603     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))?)?)?)?)?)?/);
604     if (!d) return false;
605     if (!offset) offset=0;
606     var date = new Date(d[1], 0, 1);
607
608     if (d[2]) { date.setMonth(d[2] - 1); }
609     if (d[3]) { date.setDate(d[3]); }
610     if (d[4]) { date.setHours(d[4]); }
611     if (d[5]) { date.setMinutes(d[5]); }
612     if (d[6]) { date.setSeconds(d[6]); }
613     if (d[7]) { date.setMilliseconds(Number("0." + d[7]) * 1000); }
614     if (d[8]) {
615         if (d[10] && d[11]) {
616           offset = (Number(d[10]) * 60) + Number(d[11]);
617         }
618         offset *= ((d[9] == '-') ? 1 : -1);
619         offset -= date.getTimezoneOffset();
620     }
621     var time = (Number(date) + (offset * 60 * 1000));
622     this.setTime(Number(time));
623     return true;
624   };
625 }
626
627 if (!Date.prototype.toISO8601String) {
628 /**
629  * Convert date to an ISO 8601 formatted string.
630  * @param format an integer in the range 1-6 (default is 6):<dl>
631  * <dt>1 (year)</dt>
632  *   <dd>YYYY (eg 1997)</dd>
633  * <dt>2 (year and month)</dt>
634  *   <dd>YYYY-MM (eg 1997-07)</dd>
635  * <dt>3 (complete date)</dt>
636  *   <dd>YYYY-MM-DD (eg 1997-07-16)</dd>
637  * <dt>4 (complete date plus hours and minutes)</dt>
638  *   <dd>YYYY-MM-DDThh:mmTZD (eg 1997-07-16T19:20+01:00)</dd>
639  * <dt>5 (complete date plus hours, minutes and seconds)</dt>
640  *   <dd>YYYY-MM-DDThh:mm:ssTZD (eg 1997-07-16T19:20:30+01:00)</dd>
641  * <dt>6 (complete date plus hours, minutes, seconds and a decimal
642  *   fraction of a second)</dt>
643  *   <dd>YYYY-MM-DDThh:mm:ss.sTZD (eg 1997-07-16T19:20:30.45+01:00)</dd>
644  *</dl>
645  * @see Based on: <a href='http://www.codeproject.com/jscript/dateformat.asp'>codeproject.com article</a>
646  */
647   Date.prototype.toISO8601String = function (format, offset) {
648     if (!format) format=6;
649     var date;
650     if (!offset) {
651         offset = 'Z';
652         date = this;
653     } else {
654         var d = offset.match(/([-+])([0-9]{2}):([0-9]{2})/);
655         var offsetnum = (Number(d[2]) * 60) + Number(d[3]);
656         offsetnum *= ((d[1] == '-') ? -1 : 1);
657         date = new Date(Number(Number(this) + (offsetnum * 60000)));
658     }
659
660     var zeropad = function (num) { return ((num < 10) ? '0' : '') + num; };
661
662     var str = date.getUTCFullYear();
663     if (format > 1) { str += "-" + zeropad(date.getUTCMonth() + 1); }
664     if (format > 2) { str += "-" + zeropad(date.getUTCDate()); }
665     if (format > 3) {
666         str += "T" + zeropad(date.getUTCHours()) +
667                ":" + zeropad(date.getUTCMinutes());
668     }
669     if (format > 5) {
670       var secs = Number(date.getUTCSeconds() + "." +
671                  ((date.getUTCMilliseconds() < 100) ? '0' : '') +
672                  zeropad(date.getUTCMilliseconds()));
673       str += ":" + zeropad(secs);
674     } else if (format > 4) {
675       str += ":" + zeropad(date.getUTCSeconds());
676     }
677
678     if (format > 3) { str += offset; }
679     return str;
680   };
681 }
682
683 if (!String.prototype.toISO8601Date) {
684 /**
685  * Convert string in ISO 8601 format to a date
686  * @returns new date object
687  */
688   String.prototype.toISO8601Date = function() {
689     var d = new Date();
690     return d.setISO8601(this) ? d : null;
691   };
692 }
693
694 if (!String.prototype.formatDate) {
695 /**
696  * Format string containing a date 
697  * @see Date#formatDate
698  */
699   String.prototype.formatDate = function(fmt) {
700     var s=this.replace(/-/g,'/');
701     var d = new Date(s);
702     return isNaN(d) ? this : d.formatDate(fmt);
703   };
704 }
705
706 if (!Number.prototype.formatNumber) {
707 /**
708  * Format a number according to the specs in fmt object.
709  * @returns string, wrapped in a span element with a class of: negNumber, zeroNumber, posNumber
710  * These classes can be set in CSS to display negative numbers in red, for example.
711  *
712  * @param fmt may contain any of the following:<dl>
713  *   <dt>multiplier </dt><dd> the original number is multiplied by this amount before formatting</dd>
714  *   <dt>decPlaces  </dt><dd> number of digits to the right of the decimal point</dd>
715  *   <dt>decPoint   </dt><dd> character to be used as the decimal point</dd>
716  *   <dt>thouSep    </dt><dd> character to use as the thousands separator</dd>
717  *   <dt>prefix     </dt><dd> string added to the beginning of the result (e.g. a currency symbol)</dd>
718  *   <dt>suffix     </dt><dd> string added to the end of the result (e.g. % symbol)</dd>
719  *   <dt>negSign    </dt><dd> specifies format for negative numbers: L=leading minus, T=trailing minus, P=parens</dd>
720  *</dl>
721  */
722   Number.prototype.formatNumber = function(fmt) {
723     if (isNaN(this)) return 'NaN';
724     var n=this;
725     if (typeof fmt.multiplier=='number') n*=fmt.multiplier;
726     var decPlaces=typeof fmt.decPlaces=='number' ? fmt.decPlaces : 0;
727     var thouSep=typeof fmt.thouSep=='string' ? fmt.thouSep : RicoTranslate.thouSep;
728     var decPoint=typeof fmt.decPoint=='string' ? fmt.decPoint : RicoTranslate.decPoint;
729     var prefix=fmt.prefix || "";
730     var suffix=fmt.suffix || "";
731     var negSign=typeof fmt.negSign=='string' ? fmt.negSign : "L";
732     negSign=negSign.toUpperCase();
733     var s,cls;
734     if (n<0.0) {
735       s=RicoUtil.formatPosNumber(-n,decPlaces,thouSep,decPoint);
736       if (negSign=="P") s="("+s+")";
737       s=prefix+s;
738       if (negSign=="L") s="-"+s;
739       if (negSign=="T") s+="-";
740       cls='negNumber';
741     } else {
742       cls=n==0.0 ? 'zeroNumber' : 'posNumber';
743       s=prefix+RicoUtil.formatPosNumber(n,decPlaces,thouSep,decPoint);
744     }
745     return "<span class='"+cls+"'>"+s+suffix+"</span>";
746   };
747 }
748
749 if (!String.prototype.formatNumber) {
750 /**
751  * Take a string that can be converted via parseFloat
752  * and format it according to the specs in fmt object.
753  * Number in string may use a period or comma as the decimal point,
754  * but should not contain any thousands separator.
755  */
756   String.prototype.formatNumber = function(fmt) {
757     var n=parseFloat(this.replace(/,/,'.'));
758     return isNaN(n) ? this : n.formatNumber(fmt);
759   };
760 }
761
762 Rico.Shim = Class.create();
763 /** @lends Rico.Shim# */
764 if (Prototype.Browser.IE) {
765   Rico.Shim.prototype = {
766 /**
767  * @class Fixes select control bleed-thru on floating divs in IE. Used by Rico.Popup.
768  * @see Based on <a href='http://www.dotnetjunkies.com/WebLog/jking/archive/2003/07/21/488.aspx'>technique published by Joe King</a>
769  * @constructs
770  */
771     initialize: function(DivRef) {
772       this.ifr = document.createElement('iframe');
773       this.ifr.style.position="absolute";
774       this.ifr.style.display = "none";
775       this.ifr.style.top     = '0px';
776       this.ifr.style.left    = '0px';
777       this.ifr.src="javascript:false;";
778       DivRef.parentNode.appendChild(this.ifr);
779       this.DivRef=DivRef;
780     },
781
782     hide: function() {
783       this.ifr.style.display = "none";
784     },
785
786     move: function() {
787       this.ifr.style.top  = this.DivRef.style.top;
788       this.ifr.style.left = this.DivRef.style.left;
789     },
790
791     show: function() {
792       this.ifr.style.width   = this.DivRef.offsetWidth;
793       this.ifr.style.height  = this.DivRef.offsetHeight;
794       this.move();
795       this.ifr.style.zIndex  = this.DivRef.currentStyle.zIndex - 1;
796       this.ifr.style.display = "block";
797     }
798   };
799 } else {
800   Rico.Shim.prototype = {
801 /** @ignore */
802     initialize: function() {},
803 /** @ignore */
804     hide: function() {},
805 /** @ignore */
806     move: function() {},
807 /** @ignore */
808     show: function() {}
809   };
810 }
811
812
813 Rico.Shadow = Class.create(
814 /** @lends Rico.Shadow# */
815 {
816 /**
817  * @class Creates a shadow for positioned elements. Used by Rico.Popup.
818  * Uses blur filter in IE, and alpha-transparent png images for all other browsers.
819  * @see Based on <a href='http://www.positioniseverything.net/articles/dropshadows.html'>positioniseverything article</a>
820  * @constructs
821  */
822   initialize: function(DivRef) {
823     this.div = document.createElement('div');
824     this.div.style.position="absolute";
825     this.div.style.top='0px';
826     this.div.style.left='0px';
827     if (typeof this.div.style.filter=='undefined') {
828       new Image().src = Rico.imgDir+"shadow.png";
829       new Image().src = Rico.imgDir+"shadow_ur.png";
830       new Image().src = Rico.imgDir+"shadow_ll.png";
831       this.createShadow();
832       this.offset=5;
833     } else {
834       this.div.style.backgroundColor='#888';
835       this.div.style.filter='progid:DXImageTransform.Microsoft.Blur(makeShadow=1, shadowOpacity=0.3, pixelRadius=3)';
836       this.offset=0; // MS blur filter already does offset
837     }
838     this.div.style.display = "none";
839     DivRef.parentNode.appendChild(this.div);
840     this.DivRef=DivRef;
841   },
842
843   createShadow: function() {
844     var tab = document.createElement('table');
845     tab.style.height='100%';
846     tab.style.width='100%';
847     tab.cellSpacing=0;
848     tab.dir='ltr';
849
850     var tr1=tab.insertRow(-1);
851     tr1.style.height='8px';
852     var td11=tr1.insertCell(-1);
853     td11.style.width='8px';
854     var td12=tr1.insertCell(-1);
855     td12.style.background="transparent url("+Rico.imgDir+"shadow_ur.png"+") no-repeat right bottom";
856
857     var tr2=tab.insertRow(-1);
858     var td21=tr2.insertCell(-1);
859     td21.style.background="transparent url("+Rico.imgDir+"shadow_ll.png"+") no-repeat right bottom";
860     var td22=tr2.insertCell(-1);
861     td22.style.background="transparent url("+Rico.imgDir+"shadow.png"+") no-repeat right bottom";
862
863     this.div.appendChild(tab);
864   },
865
866   hide: function() {
867     this.div.style.display = "none";
868   },
869
870   move: function() {
871     this.div.style.top  = (parseInt(this.DivRef.style.top || '0',10)+this.offset)+'px';
872     this.div.style.left = (parseInt(this.DivRef.style.left || '0',10)+this.offset)+'px';
873   },
874
875   show: function() {
876     this.div.style.width = this.DivRef.offsetWidth + 'px';
877     this.div.style.height= this.DivRef.offsetHeight + 'px';
878     this.move();
879     this.div.style.zIndex= parseInt(Element.getStyle(this.DivRef,'z-index'),10) - 1;
880     this.div.style.display = "block";
881   }
882 });
883
884
885 Rico.Popup = Class.create(
886 /** @lends Rico.Popup# */
887 {
888 /**
889  * @class Class to manage pop-up div windows.
890  * @constructs
891  * @param options object may contain any of the following:<dl>
892  *   <dt>hideOnEscape</dt><dd> hide popup when escape key is pressed? default=true</dd>
893  *   <dt>hideOnClick </dt><dd> hide popup when mouse button is clicked? default=true</dd>
894  *   <dt>ignoreClicks</dt><dd> if true, mouse clicks within the popup are not allowed to bubble up to parent elements</dd>
895  *   <dt>position    </dt><dd> defaults to absolute</dd>
896  *   <dt>shadow      </dt><dd> display shadow with popup? default=true</dd>
897  *   <dt>margin      </dt><dd> number of pixels to allow for shadow, default=6</dd>
898  *   <dt>zIndex      </dt><dd> which layer? default=1</dd>
899  *   <dt>overflow    </dt><dd> how to handle content that overflows div? default=auto</dd>
900  *   <dt>canDragFunc </dt><dd> boolean value (or function that returns a boolean) indicating if it is ok to drag/reposition popup, default=false</dd>
901  *</dl>
902  * @param DivRef if supplied, then setDiv() is called at the end of initialization
903  */
904   initialize: function(options,DivRef,closeFunc) {
905     this.options = {
906       hideOnEscape  : true,
907       hideOnClick   : true,
908       ignoreClicks  : false,
909       position      : 'absolute',
910       shadow        : true,
911       margin        : 6,
912       zIndex        : 1,
913       overflow      : 'auto',
914       canDragFunc   : false
915     };
916     Object.extend(this.options, options || {});
917     if (DivRef) this.setDiv(DivRef,closeFunc);
918   },
919
920 /**
921  * Apply popup behavior to a div that already exists in the DOM
922  * @param DivRef div element in the DOM
923  * @param closeFunc optional callback function when popup is closed
924  */
925   setDiv: function(DivRef,closeFunc) {
926     this.divPopup=$(DivRef);
927     var position=this.options.position == 'auto' ? Element.getStyle(this.divPopup,'position').toLowerCase() : this.options.position;
928     if (!this.divPopup || position != 'absolute') return;
929     this.closeFunc=closeFunc || this.closePopup.bindAsEventListener(this);
930     this.shim=new Rico.Shim(this.divPopup);
931     if (this.options.shadow)
932       this.shadow=new Rico.Shadow(this.divPopup);
933     if (this.options.hideOnClick)
934       Event.observe(document,"click", this.closeFunc);
935     if (this.options.hideOnEscape)
936       Event.observe(document,"keyup", this._checkKey.bindAsEventListener(this));
937     if (this.options.canDragFunc)
938       Event.observe(this.titleDiv || this.divPopup, "mousedown", this.startDrag.bind(this));
939     if (this.options.ignoreClicks || this.options.canDragFunc) this.ignoreClicks();
940   },
941
942 /**
943  * create popup div and insert content
944  */
945   createPopup: function(parentElem, content, ht, wi, className, closeFunc) {
946     var div = document.createElement('div');
947     div.style.position=this.options.position;
948     div.style.zIndex=this.options.zIndex;
949     div.style.overflow=this.options.overflow;
950     div.style.top='0px';
951     div.style.left='0px';
952     div.style.height=ht;
953     div.style.width=wi;
954     div.className=className || 'ricoPopup';
955     if (content) div.innerHTML=content;
956     parentElem.appendChild(div);
957     this.setDiv(div,closeFunc);
958     this.contentDiv=div;
959     if (this.options.canDragFunc===true)
960       this.options.canDragFunc=this.safeDragTest.bind(this); 
961   },
962
963 /**
964  * @private Fixes problems with IE when clicking on the scrollbar
965  * Not required when calling createWindow because dragging is only applied to the title bar
966  */
967   safeDragTest: function(elem,event) {
968     return (elem.componentFromPoint && elem.componentFromPoint(event.clientX,event.clientY)!='') ?  false : elem==this.divPopup;
969   },
970
971 /**
972  * Create popup div with a title bar.
973  * height (ht) and width (wi) parameters are required and apply to the content (title adds extra height)
974  */
975   createWindow: function(title, content, ht, wi, className) {
976     var div = document.createElement('div');
977     this.titleDiv = document.createElement('div');
978     this.contentDiv = document.createElement('div');
979     this.titleDiv.className='ricoTitle';
980     this.titleDiv.innerHTML=title;
981     this.titleDiv.style.position='relative';
982     var img = document.createElement('img');
983     img.src=Rico.imgDir+"close.gif";
984     img.title=RicoTranslate.getPhraseById('close');
985     img.style.cursor='pointer';
986     img.style.position='absolute';
987     img.style.right='0px';
988     this.titleDiv.appendChild(img);
989     this.contentDiv.className='ricoContent';
990     this.contentDiv.innerHTML=content;
991     this.contentDiv.style.height=ht;
992     this.contentDiv.style.width=wi;
993     this.contentDiv.style.overflow=this.options.overflow;
994     div.style.position=this.options.position;
995     div.style.zIndex=this.options.zIndex;
996     div.style.top='0px';
997     div.style.left='0px';
998     div.style.display='none';
999     div.className=className || 'ricoWindow';
1000     div.appendChild(this.titleDiv);
1001     div.appendChild(this.contentDiv);
1002     document.body.appendChild(div);
1003     this.setDiv(div);
1004     Event.observe(img,"click", this.closePopup.bindAsEventListener(this));
1005   },
1006
1007 /** @private */
1008   ignoreClicks: function() {
1009     Event.observe(this.divPopup,"click", this._ignoreClick.bindAsEventListener(this));
1010   },
1011
1012   _ignoreClick: function(e) {
1013     if (e.stopPropagation)
1014       e.stopPropagation();
1015     else
1016       e.cancelBubble = true;
1017     return true;
1018   },
1019
1020   // event handler to process keyup events (hide menu on escape key)
1021   _checkKey: function(e) {
1022     if (RicoUtil.eventKey(e)==27) this.closeFunc();
1023     return true;
1024   },
1025
1026 /**
1027  * Move popup to specified position
1028  */
1029   move: function(left,top) {
1030     if (typeof left=='number') this.divPopup.style.left=left+'px';
1031     if (typeof top=='number') this.divPopup.style.top=top+'px';
1032     if (this.shim) this.shim.move();
1033     if (this.shadow) this.shadow.move();
1034   },
1035
1036 /** @private */
1037   startDrag : function(event){
1038     var elem=Event.element(event);
1039     var canDrag=typeof(this.options.canDragFunc)=='function' ? this.options.canDragFunc(elem,event) : this.options.canDragFunc;
1040     if (!canDrag) return;
1041     this.divPopup.style.cursor='move';
1042     this.lastMouseX = event.clientX;
1043     this.lastMouseY = event.clientY;
1044     this.dragHandler = this.drag.bindAsEventListener(this);
1045     this.dropHandler = this.endDrag.bindAsEventListener(this);
1046     Event.observe(document, "mousemove", this.dragHandler);
1047     Event.observe(document, "mouseup", this.dropHandler);
1048     Event.stop(event);
1049   },
1050
1051 /** @private */
1052   drag : function(event){
1053     var newLeft = parseInt(this.divPopup.style.left,10) + event.clientX - this.lastMouseX;
1054     var newTop = parseInt(this.divPopup.style.top,10) + event.clientY - this.lastMouseY;
1055     this.move(newLeft, newTop);
1056     this.lastMouseX = event.clientX;
1057     this.lastMouseY = event.clientY;
1058     Event.stop(event);
1059   },
1060
1061 /** @private */
1062   endDrag : function(){
1063     this.divPopup.style.cursor='';
1064     Event.stopObserving(document, "mousemove", this.dragHandler);
1065     Event.stopObserving(document, "mouseup", this.dropHandler);
1066     this.dragHandler=null;
1067     this.dropHandler=null;
1068   },
1069
1070 /**
1071  * Display popup at specified position
1072  */
1073   openPopup: function(left,top) {
1074     this.divPopup.style.display="block";
1075     if (typeof left=='number') this.divPopup.style.left=left+'px';
1076     if (typeof top=='number') this.divPopup.style.top=top+'px';
1077     if (this.shim) this.shim.show();
1078     if (this.shadow) this.shadow.show();
1079   },
1080
1081 /**
1082  * Hide popup
1083  */
1084   closePopup: function() {
1085     if (this.dragHandler) this.endDrag();
1086     if (this.shim) this.shim.hide();
1087     if (this.shadow) this.shadow.hide();
1088     this.divPopup.style.display="none";
1089   }
1090
1091 });
1092
1093 Rico.includeLoaded('ricoCommon.js');