Changes to Rico 3 as a result of regression testing with IE6, IE7, and IE8.
[infodrom/rico3] / minsrc / ricoUI.js
1 /*
2  *  (c) 2005-2009 Richard Cowin (http://openrico.org)
3  *  (c) 2005-2009 Matt Brown (http://dowdybrown.com)
4  *
5  *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
6  *  file except in compliance with the License. You may obtain a copy of the License at
7  *
8  *         http://www.apache.org/licenses/LICENSE-2.0
9  *
10  *  Unless required by applicable law or agreed to in writing, software distributed under the
11  *  License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
12  *  either express or implied. See the License for the specific language governing permissions
13  *  and limitations under the License.
14  */
15
16 Rico.applyShadow = function(elem,shadowFlag) {
17   if (typeof shadowFlag=='undefined') shadowFlag=true;
18   if (shadowFlag) Rico.addClass(elem,'ricoShadow');
19   return elem;
20 };
21
22 // ensure popups/windows get closed in the right order when the user hits escape key
23 Rico._OpenPopupList = [];
24 Rico._RemoveOpenPopup = function(popup) {
25   if (popup.openIndex >= 0 && popup.openIndex < Rico._OpenPopupList.length) Rico._OpenPopupList.splice(popup.openIndex,1);
26   popup.openIndex = -1;
27 };
28 Rico._AddOpenPopup = function(popup) {
29   popup.openIndex = Rico._OpenPopupList.push(popup) - 1;
30 };
31 Rico._checkEscKey = function(e) {
32   if (Rico.eventKey(e) != 27) return true;
33   while (Rico._OpenPopupList.length > 0) {
34     var popup = Rico._OpenPopupList.pop();
35     if (popup && popup.visible()) {
36       popup.openIndex = -1;
37       Rico.eventStop(e);
38       popup.closeFunc();
39       return false;
40     }
41   }
42   return true;
43 };
44 Rico.eventBind(document,"keyup", Rico.eventHandle(Rico,'_checkEscKey'));
45
46
47 Rico.Popup = function(containerDiv,options) {
48   this.initialize(containerDiv,options);
49 };
50
51 Rico.Popup.prototype = {
52 /**
53  * @class Class to manage pop-up div windows.
54  * @constructs
55  * @param options object may contain any of the following:<dl>
56  *   <dt>hideOnClick </dt><dd> hide popup when mouse button is clicked? default=true</dd>
57  *   <dt>ignoreClicks</dt><dd> if true, mouse clicks within the popup are not allowed to bubble up to parent elements</dd>
58  *   <dt>position    </dt><dd> defaults to absolute, use "auto" to auto-detect</dd>
59  *   <dt>shadow      </dt><dd> display shadow with popup? default=true</dd>
60  *   <dt>zIndex      </dt><dd> which layer? default=1</dd>
61  *   <dt>canDrag     </dt><dd> boolean value (or function that returns a boolean) indicating if it is ok to drag/reposition popup, default=false</dd>
62  *   <dt>onClose     </dt><dd> function to call when the popup is closed</dd>
63  *</dl>
64  * @param containerDiv if supplied, then setDiv() is called at the end of initialization
65  */
66   initialize: function(containerDiv,options) {
67     this.options = {
68       hideOnClick   : false,
69       ignoreClicks  : false,
70       position      : 'absolute',
71       shadow        : true,
72       zIndex        : 2,
73       canDrag       : false,
74       dragElement   : false,
75       closeFunc     : false
76     };
77     this.openIndex=-1;
78     if (containerDiv) this.setDiv(containerDiv,options);
79   },
80
81   createContainer: function(options) {
82     this.setDiv(document.createElement('div'), options);
83     if (options && options.parent) {
84       options.parent.appendChild(this.container);
85     } else {
86       document.getElementsByTagName("body")[0].appendChild(this.container);
87     }
88   },
89
90 /**
91  * Apply popup behavior to a div that already exists in the DOM
92  * @param containerDiv div element (or element id) in the DOM. If null, then the div is created automatically.
93  */
94   setDiv: function(containerDiv,options) {
95     Rico.extend(this.options, options || {});
96     this.container=Rico.$(containerDiv);
97     if (this.options.position == 'auto') {
98       this.position=Rico.getStyle(this.container,'position').toLowerCase();
99     } else {
100       this.position=this.container.style.position=this.options.position;
101     }
102     this.content=document.createElement('div');
103     while (this.container.firstChild) {
104       this.content.appendChild(this.container.firstChild);
105     }
106     this.container.appendChild(this.content);
107     this.content.className='RicoPopupContent';
108     this.content.style.position='relative';
109     if (this.position != 'absolute') return;
110
111     if (this.options.closeFunc) {
112       this.closeFunc=this.options.closeFunc;
113     } else {
114       var self=this;
115       this.closeFunc=function() { self.closePopup(); };
116     }
117     this.container.style.top='0px';
118     this.container.style.left='0px';
119     this.container.style.display='none';
120     if (this.options.zIndex >= 0) this.container.style.zIndex=this.options.zIndex;
121     this.content.style.zIndex=2;
122
123     if (Rico.isIE && Rico.ieVersion < 7) {
124       // create iframe shim
125       this.ifr = document.createElement('iframe');
126       this.ifr.className='RicoShim';
127       this.ifr.frameBorder=0;
128       this.ifr.src="javascript:'';";
129       this.container.appendChild(this.ifr);
130     }
131     Rico.applyShadow(this.container,this.options.shadow);
132
133     if (this.options.hideOnClick)
134       Rico.eventBind(document,"click", Rico.eventHandle(this,'_docClick'));
135     this.dragEnabled=false;
136     this.mousedownHandler = Rico.eventHandle(this,'_startDrag');
137     this.dragHandler = Rico.eventHandle(this,'_drag');
138     this.dropHandler = Rico.eventHandle(this,'_endDrag');
139     if (this.options.canDrag) this.enableDragging();
140     if (this.options.ignoreClicks || this.options.canDrag) this.ignoreClicks();
141   },
142
143   clearContent: function() {
144     this.content.innerHTML="";
145   },
146
147   setContent: function(content) {
148     this.content.innerHTML=content;
149   },
150
151   enableDragging: function() {
152     if (!this.dragEnabled && this.options.dragElement) {
153       Rico.eventBind(this.options.dragElement, "mousedown", this.mousedownHandler);
154       this.dragEnabled=true;
155     }
156     return this.dragEnabled;
157   },
158
159   disableDragging: function() {
160     if (!this.dragEnabled) return;
161     Rico.eventUnbind(this.options.dragElement, "mousedown", this.mousedownHandler);
162     this.dragEnabled=false;
163   },
164
165   setZ: function(zIndex) {
166     this.container.style.zIndex=zIndex;
167   },
168
169 /** @private */
170   ignoreClicks: function() {
171     Rico.eventBind(this.container,"click", Rico.eventHandle(this,'_ignoreClick'));
172   },
173
174   _ignoreClick: function(e) {
175     if (e.stopPropagation)
176       e.stopPropagation();
177     else
178       e.cancelBubble = true;
179     return true;
180   },
181
182   _docClick: function(e) {
183     this.closeFunc();
184     return true;
185   },
186
187 /**
188  * Move popup to specified position
189  */
190   move: function(left,top) {
191     if (typeof left=='number') this.container.style.left=left+'px';
192     if (typeof top=='number') this.container.style.top=top+'px';
193   },
194
195   _startDrag : function(event){
196     var elem=Rico.eventElement(event);
197     this.container.style.cursor='move';
198     this.lastMouse = Rico.eventClient(event);
199     Rico.eventBind(document, "mousemove", this.dragHandler);
200     Rico.eventBind(document, "mouseup", this.dropHandler);
201     Rico.eventStop(event);
202   },
203
204   _drag : function(event){
205     var newMouse = Rico.eventClient(event);
206     var newLeft = parseInt(this.container.style.left,10) + newMouse.x - this.lastMouse.x;
207     var newTop = parseInt(this.container.style.top,10) + newMouse.y - this.lastMouse.y;
208     this.move(newLeft, newTop);
209     this.lastMouse = newMouse;
210     Rico.eventStop(event);
211   },
212
213   _endDrag : function(){
214     this.container.style.cursor='';
215     Rico.eventUnbind(document, "mousemove", this.dragHandler);
216     Rico.eventUnbind(document, "mouseup", this.dropHandler);
217   },
218
219 /**
220  * Display popup at specified position
221  */
222   openPopup: function(left,top) {
223     if (typeof left=='number') this.container.style.left=left+'px';
224     if (typeof top=='number') this.container.style.top=top+'px';
225     this.container.style.display=''; //this.position=='absolute' ? "block" : Rico.isIE && Rico.ieVersion<8 ? "inline" : "inline-block";
226     if (this.container.id) Rico.log('openPopup '+this.container.id+' at '+left+','+top);
227     Rico._AddOpenPopup(this);
228   },
229   
230   centerPopup: function() {
231     this.openPopup();
232     var msgWidth=this.container.offsetWidth;
233     var msgHeight=this.container.offsetHeight;
234     var divwi=this.container.parentNode.offsetWidth;
235     var divht=this.container.parentNode.offsetHeight;
236     this.move(parseInt(Math.max((divwi-msgWidth)/2,0),10), parseInt(Math.max((divht-msgHeight)/2,0),10));
237   },
238
239   visible: function() {
240     return Rico.visible(this.container);
241   },
242
243 /**
244  * Hide popup
245  */
246   closePopup: function() {
247     Rico._RemoveOpenPopup(this);
248     if (!Rico.visible(this.container)) return;
249     if (this.container.id) Rico.log('closePopup '+this.container.id);
250     if (this.dragEnabled) this._endDrag();
251     this.container.style.display="none";
252     if (this.options.onClose) this.options.onClose();
253   }
254
255 };
256
257 Rico.closeButton = function(handle) {
258   var a = document.createElement('a');
259   a.className='RicoCloseAnchor';
260   if (Rico.theme.closeAnchor) Rico.addClass(a,Rico.theme.closeAnchor);
261   var span = a.appendChild(document.createElement('span'));
262   span.title=Rico.getPhraseById('close');
263   new Rico.HoverSet([a]);
264   Rico.addClass(span,Rico.theme.close || 'rico-icon RicoClose');
265   Rico.eventBind(a,"click", handle);
266   return a;
267 };
268
269 Rico.floatButton = function(buttonName, handle, title) {
270   var a=document.createElement("a");
271   a.className='RicoButtonAnchor'
272   Rico.addClass(a,Rico.theme.buttonAnchor || 'RicoButtonAnchorNative');
273   var span=a.appendChild(document.createElement("span"));
274   if (title) span.title=title;
275   span.className=Rico.theme[buttonName.toLowerCase()] || 'rico-icon Rico'+buttonName;
276   Rico.eventBind(a,"click", handle, false);
277   new Rico.HoverSet([a]);
278   return a
279 }
280
281 Rico.clearButton = function(handle) {
282   var span=document.createElement("span");
283   span.title=Rico.getPhraseById('clear');
284   span.className='ricoClear';
285   Rico.addClass(span, Rico.theme.clear || 'rico-icon ricoClearNative');
286   Rico.eventBind(span,"click", handle);
287   return span;
288 }
289
290 Rico.Window = function(title, options, contentParam) {
291   this.initialize(title, options, contentParam);
292 };
293
294 Rico.Window.prototype = {
295
296 /**
297  * Create popup div with a title bar.
298  */
299   initialize: function(title, options, contentParam) {
300     options=options || {overflow:'auto'};
301     Rico.extend(this, new Rico.Popup());
302
303     this.titleDiv = document.createElement('div');
304     this.options.canDrag=true;
305     this.options.dragElement=this.titleDiv;
306     this.createContainer(options);
307     this.content.appendChild(this.titleDiv);
308     contentParam=Rico.$(contentParam);
309     this.contentDiv=contentParam || document.createElement('div');
310     this.content.appendChild(this.contentDiv);
311
312     // create title area
313     this.titleDiv.className='ricoTitle';
314     if (Rico.theme.dialogTitle) Rico.addClass(this.titleDiv,Rico.theme.dialogTitle);
315     this.titleDiv.style.position='relative';
316     this.titleContent = document.createElement('span');
317     this.titleContent.className='ricoTitleSpan';
318     this.titleDiv.appendChild(this.titleContent);
319     this.titleDiv.appendChild(Rico.closeButton(Rico.eventHandle(this,'closeFunc')));
320     if (!title && contentParam) {
321       title=contentParam.title;
322       contentParam.title='';
323     }
324     this.setTitle(title || '&nbsp;');
325
326     // create content area
327     this.contentDiv.className='ricoContent';
328     if (Rico.theme.dialogContent) Rico.addClass(this.contentDiv,Rico.theme.dialogContent);
329     this.contentDiv.style.position='relative';
330     if (options.height) this.contentDiv.style.height=options.height;
331     if (options.width) this.contentDiv.style.width=options.width;
332     if (options.overflow) this.contentDiv.style.overflow=options.overflow;
333     Rico.addClass(this.content,'ricoWindow');
334     if (Rico.theme.dialog) Rico.addClass(this.content,Rico.theme.dialog);
335     /*
336     if (Rico.isIE) {
337       // fix float'ed content in IE
338       this.titleDiv.style.zoom=1;
339       this.contentDiv.style.zoom=1;
340     }
341     */
342     this.content=this.contentDiv;
343   },
344
345   setTitle: function(title) {
346     this.titleContent.innerHTML=title;
347   }
348
349 }
350
351
352 Rico.Menu = function(options) {
353   this.initialize(options);
354 }
355
356 Rico.Menu.prototype = {
357 /**
358  * @class Implements popup menus and submenus
359  * @extends Rico.Popup
360  * @constructs
361  */
362   initialize: function(options) {
363     Rico.extend(this, new Rico.Popup());
364     Rico.extend(this.options, {
365       width        : "15em",
366       arrowColor   : "b",   // for submenus: b=black, w=white
367       showDisabled : false,
368       hideOnClick  : true
369     });
370     if (typeof options=='string')
371       this.options.width=options;
372     else
373       Rico.extend(this.options, options || {});
374     this.hideFunc=null;
375     this.highlightElem=null;
376   },
377
378   createDiv: function(parentNode) {
379     if (this.container) return;
380     var self=this;
381     var options={ closeFunc: function() { self.cancelmenu(); } };
382     if (parentNode) options.parent=parentNode;
383     this.createContainer(options);
384     this.content.className = Rico.isWebKit ? 'ricoMenuSafari' : 'ricoMenu';
385     this.content.style.width=this.options.width;
386     this.direction=Rico.getStyle(this.container,'direction') || 'ltr';
387     this.direction=this.direction.toLowerCase();  // ltr or rtl
388     this.hidemenu();
389     this.itemCount=0;
390   },
391
392   showmenu: function(e,hideFunc){
393     Rico.eventStop(e);
394     this.hideFunc=hideFunc;
395     if (this.content.childNodes.length==0) {
396       this.cancelmenu();
397       return false;
398     }
399     var mousePos = Rico.eventClient(e);
400     this.openmenu(mousePos.x,mousePos.y,0,0);
401   },
402
403   openmenu: function(x,y,clickItemWi,clickItemHt,noOffset) {
404     var newLeft=x + (noOffset ? 0 : Rico.docScrollLeft());
405     this.container.style.visibility="hidden";
406     this.container.style.display="block";
407     var w=this.container.offsetWidth;
408     var cw=this.content.offsetWidth;
409     //window.status='openmenu: newLeft='+newLeft+' width='+w+' clickItemWi='+clickItemWi+' windowWi='+Rico.windowWidth();
410     if (this.direction == 'rtl') {
411       if (newLeft > w+clickItemWi) newLeft-=cw+clickItemWi;
412     } else {
413       if (x+w > Rico.windowWidth()) newLeft-=cw+clickItemWi-2;
414     }
415     var scrTop=Rico.docScrollTop();
416     var newTop=y + (noOffset ? 0 : scrTop);
417     if (y+this.container.offsetHeight-scrTop > Rico.windowHeight())
418       newTop=Math.max(newTop-this.content.offsetHeight+clickItemHt,0);
419     this.openPopup(newLeft,newTop);
420     this.container.style.visibility ="visible";
421     return false;
422   },
423
424   clearMenu: function() {
425     this.clearContent();
426     this.defaultAction=null;
427     this.itemCount=0;
428   },
429
430   addMenuHeading: function(hdg) {
431     var el=document.createElement('div');
432     el.innerHTML=hdg;
433     el.className='ricoMenuHeading';
434     this.content.appendChild(el);
435   },
436
437   addMenuBreak: function() {
438     var brk=document.createElement('div');
439     brk.className="ricoMenuBreak";
440     this.content.appendChild(brk);
441   },
442
443   addSubMenuItem: function(menutext, submenu, translate) {
444     var dir=this.direction=='rtl' ? 'left' : 'right';
445     var a=this.addMenuItem(menutext,null,true,null,translate);
446     a.className='ricoSubMenu';
447     var arrowdiv = a.appendChild(document.createElement('div'));
448     arrowdiv.className='rico-icon rico-'+dir+'-'+this.options.arrowColor;
449     Rico.setStyle(arrowdiv,{position:'absolute',top:'2px'});
450     arrowdiv.style[dir]='0px';
451     a.RicoSubmenu=submenu;
452     Rico.eventBind(a,"mouseover", Rico.eventHandle(this,'showSubMenu'));
453     //Rico.eventBind(a,"mouseout", Rico.eventHandle(this,'subMenuOut'));
454   },
455
456   showSubMenu: function(e) {
457     if (this.openSubMenu) this.hideSubMenu();
458     var a=Rico.eventElement(e);
459     if (!a.RicoSubmenu) a=a.parentNode; // event can happen on arrow div
460     if (!a.RicoSubmenu) return;
461     this.openSubMenu=a.RicoSubmenu;
462     this.openMenuAnchor=a;
463     if (Rico.hasClass(a,'ricoSubMenu')) {
464       Rico.removeClass(a,'ricoSubMenu');
465       Rico.addClass(a,'ricoSubMenuOpen');
466     }
467     a.RicoSubmenu.openmenu(parseInt(this.container.style.left)+a.offsetWidth, parseInt(this.container.style.top)+a.offsetTop, a.offsetWidth-2, a.offsetHeight+2,true);
468   },
469
470   /*
471   subMenuOut: function(e) {
472     if (!this.openSubMenu) return;
473     Rico.eventStop(e);
474     var elem=Rico.eventElement(e);
475     var reltg = Rico.eventRelatedTarget(e) || e.toElement;
476     try {
477       while (reltg != null && reltg != this.openSubMenu.div)
478         reltg=reltg.parentNode;
479     } catch(err) {}
480     if (reltg == this.openSubMenu.div) return;
481     this.hideSubMenu();
482   },
483   */
484
485   hideSubMenu: function() {
486     if (this.openMenuAnchor) {
487       Rico.removeClass(this.openMenuAnchor,'ricoSubMenuOpen');
488       Rico.addClass(this.openMenuAnchor,'ricoSubMenu');
489       this.openMenuAnchor=null;
490     }
491     if (this.openSubMenu) {
492       this.openSubMenu.hidemenu();
493       this.openSubMenu=null;
494     }
495   },
496
497   addMenuItemId: function(phraseId,action,enabled,title,target) {
498     if ( arguments.length < 3 ) enabled=true;
499     this.addMenuItem(Rico.getPhraseById(phraseId),action,enabled,title,target);
500   },
501
502 // if action is a string, then it is assumed to be a URL and the target parm can be used indicate which window gets the content
503 // action can also be a function
504 // action can also be a Rico.eventHandle, but set target='event' in this case
505   addMenuItem: function(menutext,action,enabled,title,target) {
506     if (arguments.length >= 3 && !enabled && !this.options.showDisabled) return null;
507     this.itemCount++;
508     var a = document.createElement(typeof action=='string' ? 'a' : 'div');
509     if ( arguments.length < 3 || enabled ) {
510       if (typeof action=='string') {
511         a.href = action;
512         if (target) a.target = target;
513       } else if (target=='event') {
514         Rico.eventBind(a,"click", action);
515       } else {
516         a.onclick=action;
517       }
518       a.className = 'enabled';
519       if (this.defaultAction==null) this.defaultAction=action;
520     } else {
521       a.disabled = true;
522       a.className = 'disabled';
523     }
524     a.innerHTML = menutext;
525     if (typeof title=='string')
526       a.title = title;
527     a=this.content.appendChild(a);
528     Rico.eventBind(a,"mouseover", Rico.eventHandle(this,'mouseOver'));
529     Rico.eventBind(a,"mouseout", Rico.eventHandle(this,'mouseOut'));
530     return a;
531   },
532
533   mouseOver: function(e) {
534     if (this.highlightElem && this.highlightElem.className=='enabled-hover') {
535       // required for Safari
536       this.highlightElem.className='enabled';
537       this.highlightElem=null;
538     }
539     var elem=Rico.eventElement(e);
540     if (elem.parentNode == this.openMenuAnchor) elem=elem.parentNode;
541     if (this.openMenuAnchor && this.openMenuAnchor!=elem)
542       this.hideSubMenu();
543     if (elem.className=='enabled') {
544       elem.className='enabled-hover';
545       this.highlightElem=elem;
546     }
547   },
548
549   mouseOut: function(e) {
550     var elem=Rico.eventElement(e);
551     if (elem.className=='enabled-hover') elem.className='enabled';
552     if (this.highlightElem==elem) this.highlightElem=null;
553   },
554
555   cancelmenu: function() {
556     if (!this.visible()) return;
557     if (this.hideFunc) this.hideFunc();
558     this.hideFunc=null;
559     this.hidemenu();
560   },
561
562   hidemenu: function() {
563     if (this.openSubMenu) this.openSubMenu.hidemenu();
564     this.closePopup();
565   }
566
567 }
568
569
570 Rico.SelectionSet = function(selectionSet, options) {
571   this.initialize(selectionSet, options);
572 }
573
574 Rico.SelectionSet.prototype = {
575 /**
576  * @class
577  * @constructs
578  * @param selectionSet collection of DOM elements (or a CSS selection string)
579  * @param options object may contain any of the following:<dl>
580  *   <dt>selectedClass</dt><dd>class name to add when element is selected, default is "selected"</dd>
581  *   <dt>selectNode   </dt><dd>optional function that returns the element to be selected</dd>
582  *   <dt>onSelect     </dt><dd>optional function that gets called when element is selected</dd>
583  *   <dt>onFirstSelect</dt><dd>optional function that gets called the first time element is selected</dd>
584  *   <dt>noDefault    </dt><dd>when true, no element in the set is initially selected, default is false</dd>
585  *   <dt>selectedIndex</dt><dd>index of the element that should be initially selected, default is 0</dd>
586  *   <dt>cookieName   </dt><dd>optional name of cookie to use to remember selected element. If specified, and the cookie exists, then the cookie value overrides selectedIndex.</dd>
587  *   <dt>cookieDays   </dt><dd>specifies how long cookie should persist (in days). If unspecified, then the cookie persists for the current session.</dd>
588  *   <dt>cookiePath   </dt><dd>optional cookie path</dd>
589  *   <dt>cookieDomain </dt><dd>optional cookie domain</dd>
590  *</dl>
591  */
592   initialize: function(selectionSet, options){
593     Rico.log('SelectionSet#initialize');
594     this.options = options || {};
595     if (typeof selectionSet == 'string')
596       selectionSet = Rico.select(selectionSet);
597     this.previouslySelected = [];
598     this.selectionSet = [];
599     this.selectedClassName = this.options.selectedClass || Rico.theme.selected || "selected";
600     this.selectNode = this.options.selectNode || function(e){return e;};
601     this.onSelect = this.options.onSelect;
602     this.onFirstSelect = this.options.onFirstSelect;
603     var self=this;
604     this.clickHandler = function(idx) { self.selectIndex(idx); };
605     this.selectedIndex=-1;
606     for (var i=0; i<selectionSet.length; i++)
607       this.add(selectionSet[i]);
608     if (!this.options.noDefault) {
609       var cookieIndex=this.options.cookieName ? this.getCookie() : 0;
610       this.selectIndex(cookieIndex || this.options.selectedIndex || 0);
611     }
612   },
613   getCookie: function() {
614     var cookie = Rico.getCookie(this.options.cookieName);
615     if (!cookie) return 0;
616     var index = parseInt(cookie);
617     return index < this.selectionSet.length ? index : 0;
618   },
619   reset: function(){
620     this.previouslySelected = [];
621     this._notifySelected(this.selectedIndex);
622   },
623   clearSelected: function() {
624     if (this.selected)
625       Rico.removeClass(this.selectNode(this.selected), this.selectedClassName);
626   },
627   getIndex: function(element) {
628     for (var i=0; i<this.selectionSet.length; i++) {
629       if (element == this.selectionSet[i]) return i;
630     }
631     return -1;
632   },
633   select: function(element){
634     if (this.selected == element) return;
635     var i=this.getIndex(element);
636     if (i >= 0) this.selectIndex(i);
637   },
638   _notifySelected: function(index){
639     if (index < 0) return;
640     var element = this.selectionSet[index];
641     if (this.options.cookieName)
642       Rico.setCookie(this.options.cookieName, index, this.options.cookieDays, this.options.cookiePath, this.options.cookieDomain);
643     if (this.onFirstSelect && !this.previouslySelected[index]){
644       this.onFirstSelect(element, index);
645       this.previouslySelected[index] = true;
646     }
647     if (this.onSelect)
648       try{
649         this.onSelect(index);
650       } catch (e) {};
651   },
652   selectIndex: function(index){
653     if (this.selectedIndex == index || index >= this.selectionSet.length) return;
654     this.clearSelected();
655     this._notifySelected(index);
656     this.selectedIndex = index;
657     this.selected=this.selectionSet[index].element;
658     Rico.addClass(this.selectNode(this.selected), this.selectedClassName);
659   },
660   nextSelectIndex: function(){
661     return (this.selectedIndex + 1) % this.selectionSet.length;
662   },
663   nextSelectItem: function(){
664     return this.selectionSet[this.nextSelectIndex()];
665   },
666   selectNext: function(){
667     this.selectIndex(this.nextSelectIndex());
668   },
669   add: function(item){
670     var index=this.selectionSet.length;
671     this.selectionSet[index] = new Rico._SelectionItem(item,index,this.clickHandler);
672   },
673   remove: function(item){
674     if (item==this.selected) this.clearSelected();
675     var i=this.getIndex(item);
676     if (i < 0) return;
677     this.selectionSet[i].remove();
678     this.selectionSet.splice(i,1);
679   },
680   removeAll: function(){
681     this.clearSelected();
682     while (this.selectionSet.length > 0) {
683       this.selectionSet.pop().remove();
684     }
685   }
686 };
687
688
689 Rico._SelectionItem=function(element,index,callback) {
690   this.add(element,index,callback);
691 };
692
693 Rico._SelectionItem.prototype = {
694   add: function(element,index,callback) {
695     this.element=element;
696     this.index=index;
697     this.callback=callback;
698     this.handle=Rico.eventHandle(this,'click');
699     Rico.eventBind(element, "click", this.handle);
700   },
701
702   click: function(ev) {
703     this.callback(this.index);
704   },
705
706   remove: function() {
707     Rico.eventUnbind(this.element, "click", this.handle);
708   }
709 };
710
711
712 Rico.HoverSet = function(hoverSet, options) {
713   this.initialize(hoverSet, options);
714 };
715
716 Rico.HoverSet.prototype = {
717 /**
718  * @class
719  * @constructs
720  * @param hoverSet collection of DOM elements
721  * @param options object may contain any of the following:<dl>
722  *   <dt>hoverClass</dt><dd> class name to add when mouse is over element, default is "hover"</dd>
723  *   <dt>hoverNodes</dt><dd> optional function to select/filter which nodes are in the set</dd>
724  *</dl>
725  */
726   initialize: function(hoverSet, options){
727     Rico.log('HoverSet#initialize');
728     options = options || {};
729     this.hoverClass = options.hoverClass || Rico.theme.hover || "hover";
730     this.hoverFunc = options.hoverNodes || function(e){return [e];};
731     this.hoverSet=[];
732     if (!hoverSet) return;
733     for (var i=0; i<hoverSet.length; i++)
734       this.add(hoverSet[i]);
735   },
736   add: function(item) {
737     this.hoverSet.push(new Rico._HoverItem(item,this.hoverFunc,this.hoverClass));
738   },
739   removeAll: function(){
740     while (this.hoverSet.length > 0) {
741       this.hoverSet.pop().remove();
742     }
743   }
744 };
745
746
747 Rico._HoverItem=function(element,selectFunc,hoverClass) {
748   this.add(element,selectFunc,hoverClass);
749 };
750
751 Rico._HoverItem.prototype = {
752   add: function(element,selectFunc,hoverClass) {
753     this.element=element;
754     this.selectFunc=selectFunc;
755     this.hoverClass=hoverClass;
756     this.movehandle=Rico.eventHandle(this,'move');
757     this.outhandle=Rico.eventHandle(this,'mouseout');
758     Rico.eventBind(element, "mousemove", this.movehandle);
759     Rico.eventBind(element, "mouseout", this.outhandle);
760   },
761
762   move: function(ev) {
763     var elems=this.selectFunc(this.element);
764     for (var i=0; i<elems.length; i++)
765       Rico.addClass(elems[i],this.hoverClass);
766   },
767
768   mouseout: function(ev) {
769     var elems=this.selectFunc(this.element);
770     for (var i=0; i<elems.length; i++)
771       Rico.removeClass(elems[i],this.hoverClass);
772   },
773
774   remove: function() {
775     Rico.eventUnbind(element, "mousemove", this.movehandle);
776     Rico.eventUnbind(element, "mouseout", this.outhandle);
777   }
778 };
779
780
781 /** @namespace */
782 Rico.Effect = {};
783 Rico.Effect.easeIn = function(step){
784   return Math.sqrt(step);
785 };
786 Rico.Effect.easeOut = function(step){
787   return step*step;
788 };
789
790
791 /** @class core methods for transition effects */
792 Rico.ContentTransitionBase = function() {};
793 Rico.ContentTransitionBase.prototype = {
794   initBase: function(titles, contents, options) {
795     Rico.log('ContentTransitionBase#initBase');
796     if (typeof titles == 'string')
797       titles = Rico.select(titles);
798     if (typeof contents == 'string')
799       contents = Rico.select(contents);
800
801     this.options = options || {};
802     this.titles = titles;
803     this.contents = contents;
804     this.hoverSet = new Rico.HoverSet(titles, options);
805     for (var i=0; i<contents.length; i++) {
806       if (contents[i]) Rico.hide(contents[i]);
807     }
808     var self=this;
809     this.selectionSet = new Rico.SelectionSet(titles, Rico.extend(options, { onSelect: function(idx) { self._finishSelect(idx); } }));
810   },
811   reset: function(){
812     this.selectionSet.reset();
813   },
814   select: function(index) {
815     this.selectionSet.selectIndex(index);
816   },
817   _finishSelect: function(index) {
818     Rico.log('ContentTransitionBase#_finishSelect');
819     var panel = this.contents[index];
820     if (!panel) {
821       alert('Internal error: no panel @index='+index);
822       return;
823     }
824     if ( this.selected == panel) return;
825     if (this.transition){
826       if (this.selected){
827         this.transition(panel);
828       } else {
829         panel.style.display='block';
830       }
831     } else {
832       if (this.selected) Rico.hide(this.selected);
833       panel.style.display='block';
834     }
835     this.selected = panel;
836   },
837   addBase: function(title, content){
838     this.titles.push(title);
839     this.contents.push(content);
840     this.hoverSet.add(title);
841     this.selectionSet.add(title);
842     Rico.hide(content);
843     //this.selectionSet.select(title);
844   },
845   removeBase: function(title){},
846   removeAll: function(){
847     this.hoverSet.removeAll();
848     this.selectionSet.removeAll();
849   }
850 };
851
852
853 /**
854  * @class Implements accordion effect
855  * @see Rico.ContentTransitionBase#initialize for construction parameters
856  * @extends Rico.ContentTransitionBase
857  */
858 Rico.Accordion = function(element, options) {
859   this.initialize(element, options);
860 };
861
862 Rico.Accordion.prototype = Rico.extend(new Rico.ContentTransitionBase(),
863 /** @lends Rico.Accordion# */
864 {
865   initialize: function(element, options) {
866     element=Rico.$(element);
867     element.style.overflow='hidden';
868     element.className=options.accClass || Rico.theme.accordion || "Rico_accordion";
869     if (typeof options.panelWidth=='number') options.panelWidth+="px";
870     if (options.panelWidth) element.style.width = options.panelWidth;
871     var panels=Rico.getDirectChildrenByTag(element,'div');
872     var items,titles=[], contents=[];
873     for (var i=0; i<panels.length; i++) {
874       items=Rico.getDirectChildrenByTag(panels[i],'div');
875       if (items.length>=2) {
876         items[0].className=options.titleClass || Rico.theme.accTitle || "Rico_accTitle";
877         items[1].className=options.contentClass || Rico.theme.accContent || "Rico_accContent";
878         titles.push(items[0]);
879         contents.push(items[1]);
880         var a=Rico.wrapChildren(items[0],'','','a');
881         a.href="javascript:void(0)";
882       }
883     }
884     Rico.log('creating Rico.Accordion for '+element.id+' with '+titles.length+' panels');
885     this.initBase(titles, contents, options);
886     this.selected.style.height = this.options.panelHeight + "px";
887     this.totSteps=(typeof options.duration =='number' ? options.duration : 200)/25;
888   },
889   transition: function(p){
890     if (!this.options.noAnimate) {
891       this.closing=this.selected;
892       this.opening=p;
893       this.curStep=0;
894       var self=this;
895       this.timer=setInterval(function() { self.step(); },25);
896     } else {
897       p.style.height = this.options.panelHeight + "px";
898       if (this.selected) Rico.hide(this.selected);
899       p.style.display='block';
900     }
901   },
902   step: function() {
903     this.curStep++;
904     var oheight=Math.round(this.curStep/this.totSteps*this.options.panelHeight);
905     this.opening.style.height=oheight+'px';
906     this.closing.style.height=(this.options.panelHeight - oheight)+'px';
907     if (this.curStep==1) {
908       this.opening.style.paddingTop=this.opening.style.paddingBottom='0px';
909       this.opening.style.display='block';
910     }
911     if (this.curStep==this.totSteps) {
912       clearInterval(this.timer);
913       this.opening.style.paddingTop=this.opening.style.paddingBottom='';
914       Rico.hide(this.closing);
915     }
916   },
917   setPanelHeight: function(h) {
918     this.options.panelHeight = h;
919     this.selected.style.height = this.options.panelHeight + "px";
920   }
921 });
922
923
924 /**
925  * @class Implements tabbed panel effect
926  * @see Rico.ContentTransitionBase#initialize for construction parameters
927  * @extends Rico.ContentTransitionBase
928  */
929 Rico.TabbedPanel = function(element, options) {
930   this.initialize(element, options);
931 };
932
933 Rico.TabbedPanel.prototype = Rico.extend(new Rico.ContentTransitionBase(),
934 {
935   initialize: function(element, options) {
936     element=Rico.$(element);
937     options=options || {};
938     if (typeof options.panelWidth=='number') options.panelWidth+="px";
939     if (typeof options.panelHeight=='number') options.panelHeight+="px";
940     element.className=options.tabClass || Rico.theme.tabPanel || "Rico_tabPanel";
941     if (options.panelWidth) element.style.width = options.panelWidth;
942     var items = [];
943     var allKids = element.childNodes;
944     for( var i = 0 ; i < allKids.length ; i++ ) {
945       if (allKids[i] && allKids[i].tagName && allKids[i].tagName.match(/^div|ul$/i))
946         items.push(allKids[i]);
947     }
948     if (items.length < 2) return;
949     var childTag=items[0].tagName.toLowerCase()=='ul' ? 'li' : 'div';
950     items[0].className=options.navContainerClass || Rico.theme.tabNavContainer || "Rico_tabNavContainer";
951     items[0].style.listStyle='none';
952     items[1].className=options.contentContainerClass || Rico.theme.tabContentContainer || "Rico_tabContentContainer";
953     var titles=Rico.getDirectChildrenByTag(items[0], childTag);
954     var contents=Rico.getDirectChildrenByTag(items[1],'div');
955     if (!options.corners) options.corners='top';
956     for (var i=0; i<titles.length; i++) {
957       titles[i].className=options.titleClass || Rico.theme.tabTitle || "Rico_tabTitle";
958       var a=Rico.wrapChildren(titles[i],'','','a');
959       a.href="javascript:void(0)";
960       contents[i].className=options.contentClass || Rico.theme.tabContent || "Rico_tabContent";
961       if (options.panelHeight) contents[i].style.overflow='auto';
962       if (options.corners!='none') {
963         if (options.panelHdrWidth) titles[i].style.width=options.panelHdrWidth;
964         Rico.Corner.round(titles[i], Rico.theme.tabCornerOptions || options);
965       }
966     }
967     options.selectedClass=Rico.theme.tabSelected || 'selected';
968     this.initBase(titles, contents, options);
969     if (this.selected) this.transition(this.selected);
970   },
971   transition: function(p){
972     Rico.log('TabbedPanel#transition '+typeof(p));
973     if (this.selected) Rico.hide(this.selected);
974     Rico.show(p);
975     if (this.options.panelHeight) p.style.height = this.options.panelHeight;
976   }
977 });
978
979
980 /**
981  * @namespace
982  */
983 Rico.Corner = {
984
985    round: function(e, options) {
986       e = Rico.$(e);
987       this.options = {
988          corners : "all",
989          bgColor : "fromParent",
990          compact : false,
991          nativeCorners: false  // only use native corners?
992       };
993       Rico.extend(this.options, options || {});
994       if (typeof(Rico.getStyle(e,'border-radius'))=='string')
995         this._roundCornersStdCss(e);
996       else if (typeof(Rico.getStyle(e,'-webkit-border-radius'))=='string')
997         this._roundCornersWebKit(e);
998       else if (typeof(Rico.getStyle(e,'-moz-border-radius'))=='string')
999         this._roundCornersMoz(e);
1000       else if (!this.options.nativeCorners)
1001         this._roundCornersImpl(e);
1002    },
1003
1004     _roundCornersStdCss: function(e) {
1005       var radius=this.options.compact ? '4px' : '8px';
1006       if (this._hasString(this.options.corners, "all"))
1007         Rico.setStyle(e, {borderRadius:radius});
1008       else {
1009         if (this._hasString(this.options.corners, "top", "tl")) Rico.setStyle(e, {borderTopLeftRadius:radius});
1010         if (this._hasString(this.options.corners, "top", "tr")) Rico.setStyle(e, {borderTopRightRadius:radius});
1011         if (this._hasString(this.options.corners, "bottom", "bl")) Rico.setStyle(e, {borderBottomLeftRadius:radius});
1012         if (this._hasString(this.options.corners, "bottom", "br")) Rico.setStyle(e, {borderBottomRightRadius:radius});
1013       }
1014    },
1015
1016    _roundCornersWebKit: function(e) {
1017       var radius=this.options.compact ? '4px' : '8px';
1018       if (this._hasString(this.options.corners, "all"))
1019         Rico.setStyle(e, {WebkitBorderRadius:radius});
1020       else {
1021         if (this._hasString(this.options.corners, "top", "tl")) Rico.setStyle(e, {WebkitBorderTopLeftRadius:radius});
1022         if (this._hasString(this.options.corners, "top", "tr")) Rico.setStyle(e, {WebkitBorderTopRightRadius:radius});
1023         if (this._hasString(this.options.corners, "bottom", "bl")) Rico.setStyle(e, {WebkitBorderBottomLeftRadius:radius});
1024         if (this._hasString(this.options.corners, "bottom", "br")) Rico.setStyle(e, {WebkitBorderBottomRightRadius:radius});
1025       }
1026    },
1027
1028    _roundCornersMoz: function(e) {
1029       var radius=this.options.compact ? '4px' : '8px';
1030       if (this._hasString(this.options.corners, "all"))
1031         Rico.setStyle(e, {MozBorderRadius:radius});
1032       else {
1033         if (this._hasString(this.options.corners, "top", "tl")) Rico.setStyle(e, {MozBorderRadiusTopleft:radius});
1034         if (this._hasString(this.options.corners, "top", "tr")) Rico.setStyle(e, {MozBorderRadiusTopright:radius});
1035         if (this._hasString(this.options.corners, "bottom", "bl")) Rico.setStyle(e, {MozBorderRadiusBottomleft:radius});
1036         if (this._hasString(this.options.corners, "bottom", "br")) Rico.setStyle(e, {MozBorderRadiusBottomright:radius});
1037       }
1038    },
1039
1040   _roundCornersImpl: function(e) {
1041       var bgColor = this.options.bgColor == "fromParent" ? this._background(e.parentNode) : this.options.bgColor;
1042       e.style.position='relative';
1043       //this.options.numSlices = this.options.compact ? 2 : 4;
1044       if (this._hasString(this.options.corners, "all", "top", "tl")) this._createCorner(e,'top','left',bgColor);
1045       if (this._hasString(this.options.corners, "all", "top", "tr")) this._createCorner(e,'top','right',bgColor);
1046       if (this._hasString(this.options.corners, "all", "bottom", "bl")) this._createCorner(e,'bottom','left',bgColor);
1047       if (this._hasString(this.options.corners, "all", "bottom", "br")) this._createCorner(e,'bottom','right',bgColor);
1048    },
1049
1050   _createCorner: function(elem,tb,lr,bgColor) {
1051     //alert('Corner: '+tb+' '+lr+' bgColor='+typeof(bgColor));
1052     var corner = document.createElement("div");
1053     corner.className='ricoCorner';
1054     Rico.setStyle(corner,{width:'6px', height:'5px'});
1055     var borderStyle = Rico.getStyle(elem,'border-'+tb+'-style');
1056     var borderColor = borderStyle=='none' ? bgColor : Rico.getStyle(elem,'border-'+tb+'-color');
1057     //alert('Corner: '+tb+' '+borderStyle+borderColor+' '+);
1058     var pos=borderStyle=='none' ? '0px' : '-1px';
1059     corner.style[tb]=pos;
1060     corner.style[lr]=Rico.isIE && Rico.ieVersion<7 && lr=='right' && borderStyle!='none' ? '-2px' : '-1px';
1061     //corner.style[lr]='-1px';
1062     elem.appendChild(corner);
1063     var marginSizes = [ 0, 2, 3, 4, 4 ];
1064     if (tb=='bottom') marginSizes.reverse();
1065     var borderVal= borderStyle=='none' ? '0px none' : '1px solid '+borderColor;
1066     var d= lr=='left' ? 'Right' : 'Left';
1067     for (var i=0; i<marginSizes.length; i++) {
1068       var slice = document.createElement("div");
1069       Rico.setStyle(slice,{backgroundColor:bgColor,height:'1px'});
1070       slice.style['margin'+d]=marginSizes[i]+'px';
1071       slice.style['border'+d]=borderVal;
1072       corner.appendChild(slice);
1073     }
1074   },
1075
1076   _background: function(elem) {
1077      try {
1078        var actualColor = Rico.getStyle(elem, "backgroundColor");
1079
1080        // if color is tranparent, check parent
1081        // Safari returns "rgba(0, 0, 0, 0)", which means transparent
1082        if ( actualColor.match(/^(transparent|rgba\(0,\s*0,\s*0,\s*0\))$/i) && elem.parentNode )
1083           return this._background(elem.parentNode);
1084
1085        return actualColor == null ? "#ffffff" : actualColor;
1086      } catch(err) {
1087        return "#ffffff";
1088      }
1089    },
1090
1091    _hasString: function(str) {
1092      for(var i=1 ; i<arguments.length ; i++) {
1093        if (str.indexOf(arguments[i]) >= 0) return true;
1094      }
1095      return false;
1096    }
1097
1098 };
1099
1100 Rico.toColorPart = function(c) {
1101   return Rico.zFill(c, 2, 16);
1102 };
1103
1104
1105 Rico.Color = function(red, green, blue) {
1106   this.initialize(red, green, blue);
1107 };
1108
1109 Rico.Color.prototype = {
1110 /**
1111  * @class Methods to manipulate color values.
1112  * @constructs
1113  * @param red integer (0-255)
1114  * @param green integer (0-255)
1115  * @param blue integer (0-255)
1116  */
1117    initialize: function(red, green, blue) {
1118       this.rgb = { r: red, g : green, b : blue };
1119    },
1120
1121    setRed: function(r) {
1122       this.rgb.r = r;
1123    },
1124
1125    setGreen: function(g) {
1126       this.rgb.g = g;
1127    },
1128
1129    setBlue: function(b) {
1130       this.rgb.b = b;
1131    },
1132
1133    setHue: function(h) {
1134
1135       // get an HSB model, and set the new hue...
1136       var hsb = this.asHSB();
1137       hsb.h = h;
1138
1139       // convert back to RGB...
1140       this.rgb = Rico.Color.HSBtoRGB(hsb.h, hsb.s, hsb.b);
1141    },
1142
1143    setSaturation: function(s) {
1144       // get an HSB model, and set the new hue...
1145       var hsb = this.asHSB();
1146       hsb.s = s;
1147
1148       // convert back to RGB and set values...
1149       this.rgb = Rico.Color.HSBtoRGB(hsb.h, hsb.s, hsb.b);
1150    },
1151
1152    setBrightness: function(b) {
1153       // get an HSB model, and set the new hue...
1154       var hsb = this.asHSB();
1155       hsb.b = b;
1156
1157       // convert back to RGB and set values...
1158       this.rgb = Rico.Color.HSBtoRGB( hsb.h, hsb.s, hsb.b );
1159    },
1160
1161    darken: function(percent) {
1162       var hsb  = this.asHSB();
1163       this.rgb = Rico.Color.HSBtoRGB(hsb.h, hsb.s, Math.max(hsb.b - percent,0));
1164    },
1165
1166    brighten: function(percent) {
1167       var hsb  = this.asHSB();
1168       this.rgb = Rico.Color.HSBtoRGB(hsb.h, hsb.s, Math.min(hsb.b + percent,1));
1169    },
1170
1171    blend: function(other) {
1172       this.rgb.r = Math.floor((this.rgb.r + other.rgb.r)/2);
1173       this.rgb.g = Math.floor((this.rgb.g + other.rgb.g)/2);
1174       this.rgb.b = Math.floor((this.rgb.b + other.rgb.b)/2);
1175    },
1176
1177    isBright: function() {
1178       var hsb = this.asHSB();
1179       return this.asHSB().b > 0.5;
1180    },
1181
1182    isDark: function() {
1183       return ! this.isBright();
1184    },
1185
1186    asRGB: function() {
1187       return "rgb(" + this.rgb.r + "," + this.rgb.g + "," + this.rgb.b + ")";
1188    },
1189
1190    asHex: function() {
1191       return "#" + Rico.toColorPart(this.rgb.r) + Rico.toColorPart(this.rgb.g) + Rico.toColorPart(this.rgb.b);
1192    },
1193
1194    asHSB: function() {
1195       return Rico.Color.RGBtoHSB(this.rgb.r, this.rgb.g, this.rgb.b);
1196    },
1197
1198    toString: function() {
1199       return this.asHex();
1200    }
1201
1202 };
1203
1204 /**
1205  * Factory method for creating a color from an RGB string
1206  * @param hexCode a 3 or 6 digit hex string, optionally preceded by a # symbol
1207  * @returns a Rico.Color object
1208  */
1209 Rico.Color.createFromHex = function(hexCode) {
1210   if(hexCode.length==4) {
1211     var shortHexCode = hexCode;
1212     hexCode = '#';
1213     for(var i=1;i<4;i++)
1214       hexCode += (shortHexCode.charAt(i) + shortHexCode.charAt(i));
1215   }
1216   if ( hexCode.indexOf('#') == 0 )
1217     hexCode = hexCode.substring(1);
1218   if (!hexCode.match(/^[0-9A-Fa-f]{6}$/)) return null;
1219   var red   = hexCode.substring(0,2);
1220   var green = hexCode.substring(2,4);
1221   var blue  = hexCode.substring(4,6);
1222   return new Rico.Color( parseInt(red,16), parseInt(green,16), parseInt(blue,16) );
1223 };
1224
1225 /**
1226  * Retrieves the background color of an HTML element
1227  * @param elem the DOM element whose background color should be retreived
1228  * @returns a Rico.Color object
1229  */
1230 Rico.Color.createColorFromBackground = function(elem) {
1231
1232    if (!elem.style) return new Rico.Color(255,255,255);
1233    var actualColor = Rico.getStyle(elem, "background-color");
1234
1235    // if color is tranparent, check parent
1236    // Safari returns "rgba(0, 0, 0, 0)", which means transparent
1237    if ( actualColor.match(/^(transparent|rgba\(0,\s*0,\s*0,\s*0\))$/i) && elem.parentNode )
1238       return Rico.Color.createColorFromBackground(elem.parentNode);
1239
1240    if (actualColor == null) return new Rico.Color(255,255,255);
1241
1242    if ( actualColor.indexOf("rgb(") == 0 ) {
1243       var colors = actualColor.substring(4, actualColor.length - 1 );
1244       var colorArray = colors.split(",");
1245       return new Rico.Color( parseInt( colorArray[0],10 ),
1246                              parseInt( colorArray[1],10 ),
1247                              parseInt( colorArray[2],10 )  );
1248
1249    }
1250    else if ( actualColor.indexOf("#") == 0 ) {
1251       return Rico.Color.createFromHex(actualColor);
1252    }
1253    else
1254       return new Rico.Color(255,255,255);
1255 };
1256
1257 /**
1258  * Converts hue/saturation/brightness to RGB
1259  * @returns a 3-element object: r=red, g=green, b=blue.
1260  */
1261 Rico.Color.HSBtoRGB = function(hue, saturation, brightness) {
1262
1263   var red   = 0;
1264   var green = 0;
1265   var blue  = 0;
1266
1267   if (saturation == 0) {
1268      red = parseInt(brightness * 255.0 + 0.5,10);
1269      green = red;
1270      blue = red;
1271   }
1272   else {
1273       var h = (hue - Math.floor(hue)) * 6.0;
1274       var f = h - Math.floor(h);
1275       var p = brightness * (1.0 - saturation);
1276       var q = brightness * (1.0 - saturation * f);
1277       var t = brightness * (1.0 - (saturation * (1.0 - f)));
1278
1279       switch (parseInt(h,10)) {
1280          case 0:
1281             red   = (brightness * 255.0 + 0.5);
1282             green = (t * 255.0 + 0.5);
1283             blue  = (p * 255.0 + 0.5);
1284             break;
1285          case 1:
1286             red   = (q * 255.0 + 0.5);
1287             green = (brightness * 255.0 + 0.5);
1288             blue  = (p * 255.0 + 0.5);
1289             break;
1290          case 2:
1291             red   = (p * 255.0 + 0.5);
1292             green = (brightness * 255.0 + 0.5);
1293             blue  = (t * 255.0 + 0.5);
1294             break;
1295          case 3:
1296             red   = (p * 255.0 + 0.5);
1297             green = (q * 255.0 + 0.5);
1298             blue  = (brightness * 255.0 + 0.5);
1299             break;
1300          case 4:
1301             red   = (t * 255.0 + 0.5);
1302             green = (p * 255.0 + 0.5);
1303             blue  = (brightness * 255.0 + 0.5);
1304             break;
1305           case 5:
1306             red   = (brightness * 255.0 + 0.5);
1307             green = (p * 255.0 + 0.5);
1308             blue  = (q * 255.0 + 0.5);
1309             break;
1310       }
1311   }
1312
1313    return { r : parseInt(red,10), g : parseInt(green,10) , b : parseInt(blue,10) };
1314 };
1315
1316 /**
1317  * Converts RGB value to hue/saturation/brightness
1318  * @param r integer (0-255)
1319  * @param g integer (0-255)
1320  * @param b integer (0-255)
1321  * @returns a 3-element object: h=hue, s=saturation, b=brightness.
1322  * (unlike some HSB documentation which states hue should be a value 0-360, this routine returns hue values from 0 to 1.0)
1323  */
1324 Rico.Color.RGBtoHSB = function(r, g, b) {
1325
1326    var hue;
1327    var saturation;
1328    var brightness;
1329
1330    var cmax = (r > g) ? r : g;
1331    if (b > cmax)
1332       cmax = b;
1333
1334    var cmin = (r < g) ? r : g;
1335    if (b < cmin)
1336       cmin = b;
1337
1338    brightness = cmax / 255.0;
1339    if (cmax != 0)
1340       saturation = (cmax - cmin)/cmax;
1341    else
1342       saturation = 0;
1343
1344    if (saturation == 0)
1345       hue = 0;
1346    else {
1347       var redc   = (cmax - r)/(cmax - cmin);
1348       var greenc = (cmax - g)/(cmax - cmin);
1349       var bluec  = (cmax - b)/(cmax - cmin);
1350
1351       if (r == cmax)
1352          hue = bluec - greenc;
1353       else if (g == cmax)
1354          hue = 2.0 + redc - bluec;
1355       else
1356          hue = 4.0 + greenc - redc;
1357
1358       hue = hue / 6.0;
1359       if (hue < 0)
1360          hue = hue + 1.0;
1361    }
1362
1363    return { h : hue, s : saturation, b : brightness };
1364 };
1365
1366 /**
1367  * Returns a new XML document object
1368  */
1369 Rico.createXmlDocument = function() {
1370   if (document.implementation && document.implementation.createDocument) {
1371     var doc = document.implementation.createDocument("", "", null);
1372     // some older versions of Moz did not support the readyState property
1373     // and the onreadystate event so we patch it!
1374     if (doc.readyState == null) {
1375       doc.readyState = 1;
1376       doc.addEventListener("load", function () {
1377         doc.readyState = 4;
1378         if (typeof doc.onreadystatechange == "function") {
1379           doc.onreadystatechange();
1380         }
1381       }, false);
1382     }
1383     return doc;
1384   }
1385
1386   if (window.ActiveXObject)
1387       return Rico.tryFunctions(
1388         function() { return new ActiveXObject('MSXML2.DomDocument');   },
1389         function() { return new ActiveXObject('Microsoft.DomDocument');},
1390         function() { return new ActiveXObject('MSXML.DomDocument');    },
1391         function() { return new ActiveXObject('MSXML3.DomDocument');   }
1392       ) || false;
1393   return null;
1394 }