Special XML conversion code for Firefox >= 20.0
[infodrom/rico3] / minsrc / ricoLiveGridForms.js
1 /*
2  *  (c) 2005-2011 Richard Cowin (http://openrico.org)
3  *  (c) 2005-2011 Matt Brown (http://dowdybrown.com)
4  *
5  *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
6  *  file except in compliance with the License. You may obtain a copy of the License at
7  *
8  *         http://www.apache.org/licenses/LICENSE-2.0
9  *
10  *  Unless required by applicable law or agreed to in writing, software distributed under the
11  *  License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
12  *  either express or implied. See the License for the specific language governing permissions
13  *  and limitations under the License.
14  */
15
16 if(typeof Rico=='undefined') throw("LiveGridForms requires the Rico JavaScript framework");
17
18
19 Rico.TableEdit = function(liveGrid) {
20   this.initialize(liveGrid);
21 }
22
23 Rico.TableEdit.prototype = {
24 /**
25  * @class Supports editing LiveGrid data.
26  * @constructs
27  */
28   initialize: function(liveGrid) {
29     Rico.log('Rico.TableEdit initialize: '+liveGrid.tableId);
30     this.grid=liveGrid;
31     this.options = {
32       maxDisplayLen    : 20,    // max displayed text field length
33       panelHeight      : 200,   // size of tabbed panels
34       panelWidth       : 500,
35       compact          : false,    // compact corners
36       RecordName       : Rico.getPhraseById("record"),
37       updateURL        : window.location.href, // default is that updates post back to the generating page
38       showSaveMsg      : 'errors'  // disposition of database update responses (full - show full response, errors - show full response for errors and short response otherwise)
39     };
40     Rico.extend(this.options, liveGrid.options);
41     var self=this;
42     this.menu=liveGrid.menu;
43     this.menu.options.dataMenuHandler=function(grid,r,c,onBlankRow) { return self.editMenu(grid,r,c,onBlankRow); };
44     this.menu.ignoreClicks();
45     this.editText=Rico.getPhraseById("editRecord",this.options.RecordName);
46     this.cloneText=Rico.getPhraseById("cloneRecord",this.options.RecordName);
47     this.delText=Rico.getPhraseById("deleteRecord",this.options.RecordName);
48     this.addText=Rico.getPhraseById("addRecord",this.options.RecordName);
49     this.buttonHover=new Rico.HoverSet();
50     this.dateRegExp=/^\s*(\w+)(\W)(\w+)(\W)(\w+)/i;
51     this.createKeyArray();
52     if (typeof(this.options.ConfirmDeleteCol) != 'number')
53       this.options.ConfirmDeleteCol=this.keys.length > 0 ? -2 : -1;
54     this.createEditDiv();
55     this.saveMsg=Rico.$(liveGrid.tableId+'_savemsg');
56     Rico.eventBind(document,"click", Rico.eventHandle(this,'clearSaveMsg'));
57     this.extraMenuItems=[];
58     this.responseHandler=function(xhr) { self.processResponse(xhr); };
59     Rico.log("Rico.TableEdit.initialize complete");
60   },
61
62   createKeyArray: function() {
63     this.keys=[];
64     for (var i=0; i<this.grid.columns.length; i++) {
65       if (this.grid.columns[i].format && this.grid.columns[i].format.isKey)
66         this.keys.push({colidx:i});
67     }
68   },
69
70   createEditDiv: function() {
71
72     // create popup form
73
74     this.requestCount=1;
75     this.formPopup=this.createWindow();
76     Rico.addClass(this.formPopup.content.parentNode,'ricoLG_editDiv');
77     if (this.options.canEdit || this.options.canAdd) {
78       this.startForm();
79       this.createForm(this.form);
80     } else {
81       var buttonClose=this.createButton(Rico.getPhraseById("close"));
82       Rico.eventBind(buttonClose,"click", Rico.eventHandle(this,'cancelEdit'), false);
83       this.createForm(this.formPopup.contentDiv);
84     }
85     this.editDivCreated=true;
86
87     // create responseDialog
88
89     this.responseDialog = this.grid.createDiv('editResponse',document.body);
90     this.responseDialog.style.display='none';
91
92     var buttonOK = document.createElement('button');
93     buttonOK.appendChild(document.createTextNode(Rico.getPhraseById("ok")));
94     Rico.eventBind(buttonOK,"click", Rico.eventHandle(this,'ackResponse'));
95     this.responseDialog.appendChild(buttonOK);
96
97     this.responseDiv = this.grid.createDiv('editResponseText',this.responseDialog);
98
99     if (this.panelGroup) {
100       Rico.log("createEditDiv complete, requestCount="+this.requestCount);
101       Rico.runLater(50,this,'initPanelGroup');
102     }
103   },
104   
105   createWindow: function() {
106     var self=this;
107     return new Rico.Window('', {closeFunc: function() { self.makeFormInvisible(); }, overflow: this.options.ColGroups ? 'hidden' : 'auto'});
108   },
109
110   initPanelGroup: function() {
111     this.requestCount--;
112     Rico.log("initPanelGroup: "+this.requestCount);
113     if (this.requestCount>0) return;
114     var wi=parseInt(this.options.panelWidth,10);
115     if (this.form) {
116       //this.form.style.width=(wi+10)+'px';
117       if (Rico.isWebKit) this.formPopup.container.style.display='block';  // this causes display to flash briefly
118       this.options.bgColor = Rico.Color.createColorFromBackground(this.form).toString();
119     }
120     this.formPopup.container.style.display='none';
121     this.formPanels=new Rico.TabbedPanel(this.panelGroup, this.options);
122   },
123
124   notEmpty: function(v) {
125     return typeof(v)!='undefined';
126   },
127
128   startForm: function() {
129     this.form = document.createElement('form');
130     /** @ignore */
131     this.form.onsubmit=function() {return false;};
132     this.form.autocomplete="off"; // seems to fix "Permission denied..." errors in FF
133     this.formPopup.contentDiv.appendChild(this.form);
134
135     var tab = document.createElement('div');
136     tab.className='ButtonBar';
137     var button=tab.appendChild(this.createButton(Rico.getPhraseById("saveRecord",this.options.RecordName)));
138     Rico.eventBind(button,"click", Rico.eventHandle(this,'TESubmit'), false);
139     button=tab.appendChild(this.createButton(Rico.getPhraseById("cancel")));
140     Rico.eventBind(button,"click", Rico.eventHandle(this,'cancelEdit'), false);
141     this.form.appendChild(tab);
142
143     // hidden fields
144     this.hiddenFields = document.createElement('div');
145     this.hiddenFields.style.display='none';
146     this.action = this.appendHiddenField(this.grid.actionId,'');
147     var i,fldSpec;
148     for (i=0; i<this.grid.columns.length; i++) {
149       fldSpec=this.grid.columns[i].format;
150       if (fldSpec && fldSpec.FormView && fldSpec.FormView=="hidden")
151         this.appendHiddenField(fldSpec.FieldName,fldSpec.ColData);
152     }
153     for (var k=0; k<this.keys.length; k++) {
154       this.keys[k].keyField = this.appendHiddenField('_k'+this.keys[k].colidx,'');
155     }
156     this.form.appendChild(this.hiddenFields);
157   },
158
159   createButton: function(buttonLabel) {
160     var button = document.createElement('a');
161     button.href='javascript:void(0)';
162     button.innerHTML=buttonLabel;
163     button.className='RicoButton';
164     if (Rico.theme.button) Rico.addClass(button,Rico.theme.button);
165     this.buttonHover.add(button);
166     return button;
167   },
168
169   createPanel: function(i) {
170     var hasFields=false;
171     for (var j=0; j<this.grid.columns.length; j++) {
172       var fldSpec=this.grid.columns[j].format;
173       if (!fldSpec) continue;
174       if (!fldSpec.EntryType) continue;
175       if (fldSpec.EntryType=='H') continue;
176       if (fldSpec.FormView && fldSpec.FormView=="hidden") continue;
177       var panelIdx=fldSpec.ColGroupIdx || 0;
178       if (panelIdx==i) {
179         hasFields=true;
180         break;
181       }
182     }
183     if (!hasFields) return null;
184     this.panelHdr[i] = document.createElement('li');
185     this.panelHdr[i].innerHTML=this.options.ColGroups[i];
186     this.panelHdrs.appendChild(this.panelHdr[i]);
187     this.panelContent[i] = document.createElement('div');
188     this.panelContents.appendChild(this.panelContent[i]);
189     this.panelActualIdx[i]=this.panelCnt++;
190     return this.createFormTable(this.panelContent[i],'tabContent');
191   },
192
193   createForm: function(parentDiv) {
194     var i,div,fldSpec,panelIdx,tables=[];
195     this.panelCnt=0;
196     this.panelHdr=[];
197     this.panelContent=[];
198     if (this.options.ColGroups) {
199       this.panelGroup = document.createElement('div');
200       this.panelGroup.className='tabPanelGroup';
201       this.panelHdrs = document.createElement('ul');
202       this.panelGroup.appendChild(this.panelHdrs);
203       this.panelContents = document.createElement('div');
204       this.panelContents.className='tabContentContainer';
205       this.panelGroup.appendChild(this.panelContents);
206       this.panelActualIdx=[];
207       parentDiv.appendChild(this.panelGroup);
208       if (this.grid.direction=='rtl') {
209         for (i=this.options.ColGroups.length-1; i>=0; i--) {
210           tables[i]=this.createPanel(i);
211         }
212       } else {
213         for (i=0; i<this.options.ColGroups.length; i++) {
214           tables[i]=this.createPanel(i);
215         }
216       }
217       parentDiv.appendChild(this.panelGroup);
218     } else {
219       div=document.createElement('div');
220       div.className='noTabContent';
221       tables[0]=this.createFormTable(div);
222       parentDiv.appendChild(div);
223     }
224     for (i=0; i<this.grid.columns.length; i++) {
225       fldSpec=this.grid.columns[i].format;
226       if (!fldSpec) continue;
227       panelIdx=fldSpec.ColGroupIdx || 0;
228       if (tables[panelIdx]) this.appendFormField(this.grid.columns[i],tables[panelIdx]);
229       if (typeof fldSpec.pattern=='string') {
230         switch (fldSpec.pattern) {
231           case 'email':
232             fldSpec.regexp=/^[_a-zA-Z0-9-]+(\.[_a-zA-Z0-9-]+)*@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*\.(([0-9]{1,3})|([a-zA-Z]{2,3})|(aero|coop|info|museum|name))$/;
233             break;
234           case 'float-unsigned':
235             fldSpec.regexp=/^\d+(\.\d+)?$/;
236             break;
237           case 'float-signed':
238             fldSpec.regexp=/^[-+]?\d+(\.\d+)?$/;
239             break;
240           case 'int-unsigned':
241             fldSpec.regexp=/^\d+$/;
242             break;
243           case 'int-signed':
244             fldSpec.regexp=/^[-+]?\d+$/;
245             break;
246           default:
247             fldSpec.regexp=new RegExp(fldSpec.pattern);
248             break;
249         }
250       }
251     }
252   },
253
254   createFormTable: function(div) {
255     var tab=document.createElement('table');
256     tab.border=0;
257     div.appendChild(tab);
258     return tab;
259   },
260
261   appendHiddenField: function(name,value) {
262     var field=Rico.createFormField(this.hiddenFields,'input','hidden',name,name);
263     field.value=value;
264     return field;
265   },
266
267   appendFormField: function(column, table) {
268     var fmt=column.format;
269     if (!fmt.EntryType) return;
270     if (fmt.EntryType=="H") return;
271     if (fmt.FormView) return;
272     Rico.log('appendFormField: '+column.displayName+' - '+fmt.EntryType);
273     var row = fmt.noFormBreak && table.rows.length > 0 ? table.rows[table.rows.length-1] : table.insertRow(-1);
274     var hdr = row.insertCell(-1);
275     column.formLabel=hdr;
276     if (hdr.noWrap) hdr.noWrap=true;
277     var entry = row.insertCell(-1);
278     if (entry.noWrap) entry.noWrap=true;
279     hdr.id='lbl_'+fmt.FieldName;
280     var field, name=fmt.FieldName;
281     switch (fmt.EntryType) {
282       case 'TA':
283       case 'tinyMCE':
284         field=Rico.createFormField(entry,'textarea',null,name);
285         field.cols=fmt.TxtAreaCols;
286         field.rows=fmt.TxtAreaRows;
287         field.innerHTML=fmt.ColData;
288         hdr.style.verticalAlign='top';
289         break;
290       case 'R':
291       case 'RL':
292         field=Rico.createFormField(entry,'div',null,name);
293         if (fmt.DescriptionField) field.RicoUpdate=fmt.DescriptionField;
294         if (fmt.MultiSelect) Rico.addClass(field, 'MultiSelect');
295         if (fmt.isNullable && !fmt.MultiSelect) this.addSelectNone(field);
296         this.selectValuesRequest(field,column);
297         break;
298       case 'N':
299         field=Rico.createFormField(entry,'select',null,name);
300         if (fmt.isNullable) this.addSelectNone(field);
301         Rico.eventBind(field,"change", Rico.eventHandle(this,'checkSelectNew'));
302         this.selectValuesRequest(field,column);
303         field=document.createElement('span');
304         field.className='ricoEditLabel';
305         field.id='labelnew__'+fmt.FieldName;
306         field.innerHTML='&nbsp;&nbsp;&nbsp;'+Rico.getPhraseById('formNewValue').replace(' ','&nbsp;');
307         entry.appendChild(field);
308         name='textnew__'+fmt.FieldName;
309         field=Rico.createFormField(entry,'input','text',name,name);
310         break;
311       case 'S':
312       case 'SL':
313         if (fmt.ReadOnly) {
314           field=Rico.createFormField(entry,'input','text',name,name);
315           this.initField(field,fmt);
316         } else {
317           field=Rico.createFormField(entry,'select',null,name);
318           if (fmt.MultiSelect) field.multiple=true;
319           if (fmt.SelectRows) field.size=parseInt(fmt.SelectRows,10);
320           if (fmt.isNullable && !fmt.MultiSelect) this.addSelectNone(field);
321           if (fmt.DescriptionField) {
322             field.RicoUpdate=fmt.DescriptionField;
323             Rico.eventBind(field,"change", Rico.eventHandle(this,'selectClick'), false);
324           }
325           this.selectValuesRequest(field,column);
326         }
327         break;
328       case 'D':
329         if (!fmt.isNullable) fmt.required=true;
330         if (!fmt.dateFmt) fmt.dateFmt=Rico.dateFmt;
331         if (!fmt.Help) fmt.Help=fmt.dateFmt;
332         if (typeof fmt.min=='string') fmt.min=Rico.setISO8601(fmt.min) || new Date(fmt.min);
333         if (typeof fmt.max=='string') fmt.max=Rico.setISO8601(fmt.max) || new Date(fmt.max);
334         fmt.Length=Math.max(fmt.dateFmt.length,10);
335         if (Rico.inputtypes.date) {
336           // use the WebForms calendar
337           field=Rico.createFormField(entry,'input','date',name,name);
338           field.required=fmt.required;
339           if (fmt.min) field.min=Rico.toISO8601String(fmt.min,3);
340           if (fmt.max) field.max=Rico.toISO8601String(fmt.max,3);
341           field.required=fmt.required;
342           fmt.SelectCtl=null;  // no need for Rico calendar control
343         } else {
344           field=Rico.createFormField(entry,'input','text',name,name);
345         }
346         this.initField(field,fmt);
347         break;
348       case 'I':
349         if (!fmt.isNullable) fmt.required=true;
350         if (!fmt.pattern) fmt.pattern='int-signed';
351         if (Rico.inputtypes.number) {
352           field=Rico.createFormField(entry,'input','number',name,name);
353           field.required=fmt.required;
354           field.min=fmt.min;
355           field.max=fmt.max;
356           field.step=1;
357         } else {
358           field=Rico.createFormField(entry,'input','text',name,name);
359         }
360         if (typeof fmt.min=='string') fmt.min=parseInt(fmt.min,10);
361         if (typeof fmt.max=='string') fmt.max=parseInt(fmt.max,10);
362         this.initField(field,fmt);
363         break;
364       case 'F':
365         if (!fmt.isNullable) fmt.required=true;
366         if (!fmt.pattern) fmt.pattern='float-signed';
367         field=Rico.createFormField(entry,'input','text',name,name);
368         this.initField(field,fmt);
369         if (typeof fmt.min=='string') fmt.min=parseFloat(fmt.min);
370         if (typeof fmt.max=='string') fmt.max=parseFloat(fmt.max);
371         break;
372       default:
373         field=Rico.createFormField(entry,'input','text',name,name);
374         if (!fmt.isNullable && fmt.EntryType!='T') fmt.required=true;
375         this.initField(field,fmt);
376         break;
377     }
378     if (field && fmt.SelectCtl) {
379       Rico.EditControls.applyTo(column,field,fmt.EntryType=='D');
380     }
381     var hdrSuffix='';
382     hdr.className='ricoEditLabel';
383     if (fmt.Help) {
384       hdr.title=fmt.Help;
385       hdrSuffix="&nbsp;<span class='rico-icon rico-info'></span>";
386     }
387     var hdrText=fmt.EntryType.length>1 && fmt.EntryType.charAt(1)=='L' ? column.next.displayName : column.displayName;
388     hdr.innerHTML=hdrText+hdrSuffix;
389   },
390
391   addSelectNone: function(field) {
392     this.addSelectOption(field,this.options.TableSelectNone,Rico.getPhraseById("selectNone"));
393   },
394
395   initField: function(field,fmt) {
396     if (fmt.Length) {
397       field.maxLength=fmt.Length;
398       field.size=Math.min(fmt.Length, this.options.maxDisplayLen);
399     }
400     field.value=fmt.ColData;
401   },
402   
403   selectClick: function(e) {
404     var SelObj=Rico.eventElement(e);
405     if (SelObj.readOnly) {
406       Rico.eventStop(e);
407       return false;
408     }
409     if (SelObj.RicoUpdate) {
410       var opt=SelObj.options[SelObj.selectedIndex];
411       Rico.$(SelObj.RicoUpdate).value=opt.innerHTML;
412     }
413   },
414   
415   radioClick: function(e) {
416     var ChkBoxObj=Rico.eventElement(e);
417     if (ChkBoxObj.readOnly) {
418       Rico.eventStop(e);
419       return false;
420     }
421     var container=Rico.getParentByTagName(ChkBoxObj,'div');
422     if (container.RicoUpdate) {
423       Rico.$(container.RicoUpdate).value=ChkBoxObj.nextSibling.innerHTML;
424     }
425   },
426
427   checkSelectNew: function(e) {
428     this.updateSelectNew(Rico.eventElement(e));
429   },
430
431   updateSelectNew: function(SelObj) {
432     var vis=(SelObj.value==this.options.TableSelectNew) ? "" : "hidden";
433     Rico.$("labelnew__" + SelObj.id).style.visibility=vis;
434     Rico.$("textnew__" + SelObj.id).style.visibility=vis;
435   },
436
437   selectValuesRequest: function(elem,column) {
438     var fldSpec=column.format;
439     if (fldSpec.SelectValues) {
440       var valueList=fldSpec.SelectValues.split(',');
441       for (var i=0; i<valueList.length; i++)
442         this.addSelectOption(elem,valueList[i],valueList[i],i);
443     } else {
444       this.requestCount++;
445       var options={}, self=this;
446       Rico.extend(options, this.grid.buffer.ajaxOptions);
447       options.parameters = this.grid.buffer.formQueryHashXML(0,-1);
448       options.parameters.edit = column.index;
449       options.onComplete = function(request) { self.selectValuesUpdate(elem,request); };
450       new Rico.ajaxRequest(this.grid.buffer.dataSource, options);
451       Rico.log("selectValuesRequest: "+fldSpec.FieldName);
452     }
453   },
454
455   selectValuesUpdate: function(elem,request) {
456     var response = request.responseXML.getElementsByTagName("ajax-response");
457     Rico.log("selectValuesUpdate: "+request.status);
458     if (response == null || response.length != 1) return;
459     response=response[0];
460     var error = response.getElementsByTagName('error');
461     if (error.length > 0) {
462       var errmsg=Rico.getContentAsString(error[0],this.grid.buffer.isEncoded);
463       Rico.log("Data provider returned an error:\n"+errmsg);
464       alert(Rico.getPhraseById("requestError",errmsg));
465       return;
466     }
467     response=response.getElementsByTagName('response')[0];
468     var rowsElement = response.getElementsByTagName('rows')[0];
469     var rows = this.grid.buffer.dom2jstable(rowsElement);
470     Rico.log("selectValuesUpdate: id="+elem.id+' rows='+rows.length);
471     for (var i=0; i<rows.length; i++) {
472       if (rows[i].length>0) {
473         var c0=rows[i][0];
474         var c1=(rows[i].length>1) ? rows[i][1] : c0;
475         this.addSelectOption(elem,c0,c1,i);
476       }
477     }
478     if (Rico.$('textnew__'+elem.id))
479       this.addSelectOption(elem,this.options.TableSelectNew,Rico.getPhraseById("selectNewVal"));
480     if (this.panelGroup)
481       Rico.runLater(50,this,'initPanelGroup');
482   },
483
484   addSelectOption: function(elem,value,text,idx) {
485     switch (elem.tagName.toLowerCase()) {
486       case 'div':
487         var opt=Rico.createFormField(elem,'input', Rico.hasClass(elem, 'MultiSelect') ? 'checkbox' : 'radio', elem.id+'_'+idx, elem.id);
488         opt.value=value;
489         var lbl=document.createElement('label');
490         lbl.innerHTML=text;
491         lbl.htmlFor=opt.id;
492         elem.appendChild(lbl);
493         Rico.eventBind(opt,"click", Rico.eventHandle(this,'radioClick'), false);
494         break;
495       case 'select':
496         Rico.addSelectOption(elem,value,text);
497         break;
498     }
499   },
500
501   clearSaveMsg: function() {
502     if (this.saveMsg) this.saveMsg.innerHTML="";
503   },
504
505   addMenuItem: function(menuText,menuAction,enabled) {
506     this.extraMenuItems.push({menuText:menuText,menuAction:menuAction,enabled:enabled});
507   },
508
509   editMenu: function(grid,r,c,onBlankRow) {
510     this.clearSaveMsg();
511     if (this.grid.buffer.sessionExpired==true || this.grid.buffer.startPos<0) return false;
512     this.rowIdx=r;
513     var elemTitle=Rico.$('pageTitle');
514     var pageTitle=elemTitle ? elemTitle.innerHTML : document.title;
515     this.menu.addMenuHeading(pageTitle);
516     var self=this;
517     if (onBlankRow==false) {
518       for (var i=0; i<this.extraMenuItems.length; i++) {
519         this.menu.addMenuItem(this.extraMenuItems[i].menuText,this.extraMenuItems[i].menuAction,this.extraMenuItems[i].enabled);
520       }
521       this.menu.addMenuItem(this.editText, function() { self.editRecord(); },this.canEdit(r));
522       this.menu.addMenuItem(this.delText, function() { self.deleteRecord(); },this.canDelete(r));
523       if (this.options.canClone) {
524         this.menu.addMenuItem(this.cloneText, function() { self.cloneRecord(); },this.canAdd(r) && this.canEdit(r));
525       }
526     }
527     this.menu.addMenuItem(this.addText, function() { self.addRecord(); },this.canAdd(r));
528     return true;
529   },
530   
531   canAdd: function(r) {
532     return (typeof this.options.canAdd=='function') ? this.options.canAdd(r) : this.options.canAdd;
533   },
534
535   canEdit: function(r) {
536     return (typeof this.options.canEdit=='function') ? this.options.canEdit(r) : this.options.canEdit;
537   },
538
539   canDelete: function(r) {
540     return (typeof this.options.canDelete=='function') ? this.options.canDelete(r) : this.options.canDelete;
541   },
542
543   cancelEdit: function(e) {
544     Rico.eventStop(e);
545     this.makeFormInvisible();
546     this.grid.highlightEnabled=true;
547     this.menu.cancelmenu();
548     return false;
549   },
550
551   setField: function(fldnum,fldvalue) {
552     var fldSpec=this.grid.columns[fldnum].format;
553     var e=Rico.$(fldSpec.FieldName);
554     var a,i,o,elems,opts,txt;
555     if (!e) return;
556     Rico.log('setField: '+fldSpec.FieldName+'='+fldvalue);
557     switch (e.tagName.toUpperCase()) {
558       case 'DIV':
559         elems=e.getElementsByTagName('INPUT');
560         o={}
561         if (fldSpec.MultiSelect && fldvalue) {
562           a=fldvalue.split(',');
563           for (var i=0; i<a.length; i++) o[a[i]]=1;
564         } else {
565           o[fldvalue]=1;
566         }
567         for (i=0; i<elems.length; i++)
568           elems[i].checked=o[elems[i].value]==1;
569         break;
570       case 'INPUT':
571         if (fldSpec.EntryType=='D') {
572           // remove time data if it exists
573           a=fldvalue.split(/\s|T/);
574           fldvalue=a[0];
575           if (this.isTextInput(e)) {
576             var d=fldvalue.toLowerCase() == 'today' ? new Date() : Rico.setISO8601(fldvalue);
577             if (d) fldvalue=Rico.formatDate(d,fldSpec.dateFmt);
578           }
579         }
580         e.value=fldvalue;
581         break;
582       case 'SELECT':
583         opts=e.options;
584         //alert('setField SELECT: id='+e.id+'\nvalue='+fldvalue+'\nopt cnt='+opts.length)
585         o={}
586         if (fldSpec.MultiSelect && fldvalue) {
587           a=fldvalue.split(',');
588           for (var i=0; i<a.length; i++) o[a[i]]=1;
589           for (i=0; i<opts.length; i++)
590             opts[i].selected=o[opts[i].value]==1;
591         } else {
592           for (i=0; i<opts.length; i++) {
593             if (opts[i].value==fldvalue) {
594               e.selectedIndex=i;
595               break;
596             }
597           }
598         }
599         if (fldSpec.EntryType=='N') {
600           txt=Rico.$('textnew__'+e.id);
601           if (!txt) alert('Warning: unable to find id "textnew__'+e.id+'"');
602           txt.value=fldvalue;
603           if (e.selectedIndex!=i) e.selectedIndex=opts.length-1;
604           this.updateSelectNew(e);
605         }
606         return;
607       case 'TEXTAREA':
608         e.value=fldvalue;
609         if (fldSpec.EntryType=='tinyMCE' && typeof(tinyMCE)!='undefined' && this.initialized) {
610           if (tinyMCE.updateContent) {
611             tinyMCE.updateContent(e.id);  // version 2.x
612           } else {
613             tinyMCE.execInstanceCommand(e.id, 'mceSetContent', false, fldvalue);  // version 3.x
614           }
615         }
616         return;
617     }
618   },
619
620   setReadOnly: function(action) {
621     for (var ro,i=0; i<this.grid.columns.length; i++) {
622       var fldSpec=this.grid.columns[i].format;
623       if (!fldSpec) continue;
624       var e=Rico.$(fldSpec.FieldName);
625       if (!e) continue;
626       switch (action) {
627         case 'ins': ro=!fldSpec.Writeable || fldSpec.ReadOnly || fldSpec.UpdateOnly; break;
628         case 'upd': ro=!fldSpec.Writeable || fldSpec.ReadOnly || fldSpec.InsertOnly; break;
629         default:    ro=false; break;
630       }
631       switch (e.tagName.toUpperCase()) {
632         case 'DIV':
633           var elems=e.getElementsByTagName('INPUT');
634           for (var j=0; j<elems.length; j++) {
635             elems[j].disabled=ro;
636           }
637           break;
638         case 'SELECT':
639           if (fldSpec.EntryType=='N') {
640             var txt=Rico.$('textnew__'+e.id);
641             txt.disabled=ro;
642           }
643           e.disabled=ro;
644           break;
645         case 'TEXTAREA':
646         case 'INPUT':
647           e.disabled=ro;
648           if (fldSpec.selectIcon) fldSpec.selectIcon.style.display=ro ? 'none' : '';
649           break;
650       }
651     }
652   },
653
654   hideResponse: function(msg) {
655     this.responseDiv.innerHTML=msg;
656     this.responseDialog.style.display='none';
657   },
658
659   showResponse: function() {
660     var offset=Rico.cumulativeOffset(this.grid.outerDiv);
661     offset.top+=Rico.docScrollTop();
662     this.responseDialog.style.top=offset.top+"px";
663     this.responseDialog.style.left=offset.left+"px";
664     this.responseDialog.style.display='';
665   },
666
667   processResponse: function(xhr) {
668     var responseText,success=true;
669     Rico.log('Processing response from form submittal: '+typeof(xhr));
670     this.responseDiv.innerHTML=xhr.responseText;
671     var respNodes=Rico.select('.ricoFormResponse',this.responseDiv);
672     if (respNodes) {
673       // generate a translated response
674       Rico.log('Found ricoFormResponse');
675       var phraseId=Rico.trim(respNodes[0].className).split(/\s+/)[1];
676       responseText=Rico.getPhraseById(phraseId,this.options.RecordName);
677     } else {
678       // present the response as sent from the server (untranslated)
679       Rico.log('Processing response text');
680       var ch=this.responseDiv.childNodes;
681       for (var i=ch.length-1; i>=0; i--) {
682         if (ch[i].nodeType==1 && ch[i].nodeName!='P' && ch[i].nodeName!='DIV' && ch[i].nodeName!='BR')
683           this.responseDiv.removeChild(ch[i]);
684       }
685       responseText=Rico.stripTags(this.responseDiv.innerHTML);
686       success=(responseText.toLowerCase().indexOf('error')==-1);
687     }
688     if (success && this.options.showSaveMsg!='full') {
689       this.hideResponse('');
690       this.grid.resetContents();
691       this.grid.buffer.foundRowCount = false;
692       this.grid.buffer.fetch(this.grid.lastRowPos || 0);
693       if (this.saveMsg) this.saveMsg.innerHTML='&nbsp;'+responseText+'&nbsp;';
694     }
695     this.processCallback(this.options.onSubmitResponse);
696     Rico.log('Processing response completed');
697   },
698
699   processCallback: function(callback) {
700     switch (typeof callback) {
701       case 'string': return eval(callback);
702       case 'function': return callback();
703     }
704   },
705
706   // called when ok pressed on error response message
707   ackResponse: function(e) {
708     this.hideResponse('');
709     this.grid.highlightEnabled=true;
710   },
711
712   cloneRecord: function() {
713     this.formPopup.setTitle(this.cloneText);
714     this.displayEditForm("ins");
715   },
716
717   editRecord: function() {
718     this.formPopup.setTitle(this.editText);
719     this.displayEditForm("upd");
720   },
721
722   displayEditForm: function(action) {
723     this.grid.highlightEnabled=false;
724     this.menu.cancelmenu();
725     this.hideResponse(Rico.getPhraseById('saving'));
726     this.grid.outerDiv.style.cursor = 'auto';
727     this.action.value=action;
728     for (var i=0; i<this.grid.columns.length; i++) {
729       var c=this.grid.columns[i];
730       if (c.format) {
731         var v=c.getValue(this.rowIdx);
732         this.setField(i,v);
733         if (c.format.selectDesc) {
734           if (c.format.EntryType.length>1 && c.format.EntryType.charAt(1)=='L')
735             v=this.grid.columns[i+1].getValue(this.rowIdx);
736           v=c._format(v);
737           if (v==='') v='&nbsp;';
738           c.format.selectDesc.innerHTML=v;
739         }
740         if (c.format.SelectCtl)
741           Rico.EditControls.displayClrImg(c, !c.format.InsertOnly);
742       }
743     }
744     this.setReadOnly(action);
745     for (var k=0; k<this.keys.length; k++) {
746       this.keys[k].keyField.value = this.grid.buffer.getWindowValue(this.rowIdx,this.keys[k].colidx);
747     }
748     this.makeFormVisible(this.rowIdx);
749   },
750   
751   addPrepare: function() {
752     this.hideResponse(Rico.getPhraseById('saving'));
753     this.form.reset();
754     this.setReadOnly("ins");
755     this.action.value="ins";
756     for (var i=0; i<this.grid.columns.length; i++) {
757       var c=this.grid.columns[i];
758       if (c.format) {
759         this.setField(i,c.format.ColData);
760         if (c.format.SelectCtl) {
761           if (c.format.EntryType != 'D') Rico.EditControls.resetValue(c);
762           Rico.EditControls.displayClrImg(c, !c.format.UpdateOnly);
763         }
764       }
765     }
766   },
767
768   addRecord: function() {
769     this.menu.cancelmenu();
770     this.formPopup.setTitle(this.addText);
771     this.addPrepare();
772     this.makeFormVisible(-1);
773     if (this.formPanels) this.formPanels.select(0);
774   },
775
776   drillDown: function(e,masterColNum,detailColNum) {
777     return this.grid.drillDown.apply(this.grid, arguments);
778   },
779
780   // set filter on a detail grid that is in a master-detail relationship
781   setDetailFilter: function(colNumber,filterValue) {
782     this.grid.setDetailFilter(colNumber,filterValue);
783   },
784
785   makeFormVisible: function(row) {
786     this.formPopup.container.style.display='block';
787
788     // set left position
789     var editWi=this.formPopup.container.offsetWidth;
790     var odOffset=Rico.cumulativeOffset(this.grid.outerDiv);
791     var winWi=Rico.windowWidth();
792     this.formPopup.container.style.left=editWi+odOffset.left > winWi ? (winWi-editWi)+'px' : (odOffset.left+1)+'px';
793
794     // set top position
795     var scrTop=Rico.docScrollTop();
796     var editHt=this.formPopup.container.offsetHeight;
797     var newTop=odOffset.top+this.grid.hdrHt+scrTop;
798     var bottom=Rico.windowHeight()+scrTop;
799     if (row >= 0) {
800       newTop+=(row+1)*this.grid.rowHeight;
801       if (newTop+editHt>bottom) newTop-=(editHt+this.grid.rowHeight);
802     } else {
803       if (newTop+editHt>bottom) newTop=bottom-editHt-2;
804     }
805
806     if (this.processCallback(this.options.formOpen) === false) return;
807     this.formPopup.openPopup(null,Math.max(newTop,scrTop));
808     this.formPopup.container.style.visibility='visible';
809     Rico.EditControls.setZ(Rico.getStyle(this.formPopup.container,'zIndex'));
810     if (this.initialized) return;
811
812     var i, spec;
813     for (i = 0; i < this.grid.columns.length; i++) {
814       spec=this.grid.columns[i].format;
815       if (!spec || !spec.EntryType || !spec.FieldName) continue;
816       switch (spec.EntryType) {
817         case 'tinyMCE':
818           if (typeof tinyMCE!='undefined') tinyMCE.execCommand('mceAddControl', true, spec.FieldName);
819           break;
820       }
821     }
822     this.initialized=true;
823   },
824
825   makeFormInvisible: function() {
826     for (var i=0; i<this.grid.columns.length; i++) {
827       if (this.grid.columns[i].format && this.grid.columns[i].format.SelectCtl)
828         Rico.EditControls.close(this.grid.columns[i].format.SelectCtl);
829     }
830     this.formPopup.container.style.visibility='hidden';
831     this.formPopup.closePopup();
832     this.processCallback(this.options.formClose);
833   },
834
835   getConfirmDesc: function(rowIdx) {
836     return Rico.stripTags(this.grid.cell(rowIdx,this.options.ConfirmDeleteCol).innerHTML).replace('&nbsp;',' '); //.unescapeHTML();
837   },
838
839   deleteRecord: function() {
840     this.menu.cancelmenu();
841     var desc;
842     switch(this.options.ConfirmDeleteCol){
843       case -1 :
844         desc=Rico.getPhraseById("thisRecord",this.options.RecordName);
845         break;
846       case -2 : // Use key/column header to identify the row
847         desc='';
848         for (var k=0; k<this.keys.length; k++) {
849           var i=this.keys[k].colidx;
850           var fmt=this.grid.columns[i].format;
851           if (fmt.EntryType.length>1 && fmt.EntryType.charAt(1)=='L') i++;
852           var value=Rico.trim(Rico.stripTags(this.grid.cell(this.rowIdx,i).innerHTML).replace(/&nbsp;/g,' '));
853           if (desc) desc+=', ';
854           desc+=this.grid.columns[i].displayName + ' \"' + value + '\"';
855         }
856         break;
857       default   :
858         desc='\"' + Rico.truncate(this.getConfirmDesc(this.rowIdx),50) + '\"';
859         break;
860     }
861     if (!this.options.ConfirmDelete.valueOf || confirm(Rico.getPhraseById("confirmDelete",desc))) {
862       this.hideResponse(Rico.getPhraseById('deleting'));
863       this.showResponse();
864       var parms={};
865       parms[this.grid.actionId]="del";
866       for (var k=0; k<this.keys.length; k++) {
867         var i=this.keys[k].colidx;
868         var value=this.grid.columns[i].getValue(this.rowIdx);
869         parms['_k'+i]=value;  // prototype does the encoding automatically
870         //parms['_k'+i]=encodeURIComponent(value);
871       }
872       new Rico.ajaxRequest(this.options.updateURL, {parameters:parms,method:'post',onComplete:this.responseHandler});
873     }
874     this.menu.cancelmenu();
875   },
876
877   validationMsg: function(elem,colnum,phraseId) {
878     var col=this.grid.columns[colnum];
879     if (this.formPanels) this.formPanels.select(this.panelActualIdx[col.format.ColGroupIdx]);
880     var label=Rico.stripTags(col.formLabel.innerHTML).replace(/&nbsp;/g,' ');
881     var msg=Rico.getPhraseById(phraseId," \"" + label + "\"");
882     Rico.log(' Validation error: '+msg);
883     if (col.format.Help) msg+="\n\n"+col.format.Help;
884     alert(msg);
885     setTimeout(function() { try { elem.focus(); elem.select(); } catch(e) {}; }, 10);
886     return false;
887   },
888   
889   isTextInput: function(elem) {
890     if (!elem) return false;
891     if (elem.tagName.toLowerCase()!='input') return false;
892     if (elem.type.toLowerCase()!='text') return false;
893     if (elem.readOnly) return false;
894     if (!Rico.visible(elem)) return false;
895     return true;
896   },
897   
898   parseDate: function(v, dateFmt) {
899     dateParts={};
900     if (!this.dateRegExp.exec(dateFmt)) return NaN;
901     dateParts[RegExp.$1]=0;
902     dateParts[RegExp.$3]=1;
903     dateParts[RegExp.$5]=2;
904     var aDate = v.split(/\D/);
905     var d=new Date();
906     var curyr=d.getFullYear();
907     if (aDate.length==2 && dateParts.yyyy==2) aDate.push(curyr);
908     if (aDate.length!=3) return NaN;
909     var dd=parseInt(aDate[dateParts.dd], 10);
910     if (dd==0 || dd>31) return NaN;
911     var mm=parseInt(aDate[dateParts.mm], 10) - 1;
912     if (mm > 11) return NaN;
913     var yy=parseInt(aDate[dateParts.yyyy], 10);
914     if (yy < 100) {
915       // apply a century to 2-digit years
916       yy+=curyr - (curyr % 100);
917     }
918     return new Date(yy,mm,dd,0,0,0);  // ensure time is midnight
919   },
920
921   TESubmit: function(e) {
922     var i,ro,lbl,spec,elem,n,dateValues=[];
923
924     Rico.eventStop(e);
925     Rico.log('Event: TESubmit called to validate input');
926
927     // check fields that are supposed to be non-blank
928
929     for (i = 0; i < this.grid.columns.length; i++) {
930       spec=this.grid.columns[i].format;
931       if (!spec || !spec.EntryType || !spec.FieldName) continue;
932       elem=Rico.$(spec.FieldName);
933       if (!this.isTextInput(elem)) continue;
934       switch (this.action.value) {
935         case 'ins': ro=!spec.Writeable || spec.ReadOnly || spec.UpdateOnly; break;
936         case 'upd': ro=!spec.Writeable || spec.ReadOnly || spec.InsertOnly; break;
937         default:    ro=false; break;
938       }
939       if (ro) continue;  // readonly, so don't validate
940       Rico.log(' Validating field #'+i+' EntryType='+spec.EntryType+' ('+spec.FieldName+')');
941
942       // check for blanks
943       if (elem.value.length == 0) {
944         if (spec.required)
945           return this.validationMsg(elem,i,"formPleaseEnter");
946         else
947           continue;
948       }
949
950       // check pattern
951       if (elem.value.length > 0 && spec.regexp && !spec.regexp.test(elem.value))
952         return this.validationMsg(elem,i,"formInvalidFmt");
953
954       // check min/max and date values
955       switch (spec.EntryType.charAt(0)) {
956         case 'I': n=parseInt(elem.value,10); break;
957         case 'F': n=parseFloat(elem.value); break;
958         case 'D': 
959           n=this.parseDate(elem.value,spec.dateFmt);
960           if (isNaN(n)) return this.validationMsg(elem,i,"formInvalidFmt");
961           dateValues.push({e:elem,v:n});
962           break;
963         default:  n=NaN; break;
964       }
965       if (typeof spec.min!='undefined' && !isNaN(n) && n < spec.min)
966         return this.validationMsg(elem,i,"formOutOfRange");
967       if (typeof spec.max!='undefined' && !isNaN(n) && n > spec.max)
968         return this.validationMsg(elem,i,"formOutOfRange");
969     }
970     if (this.processCallback(this.options.formSubmit) === false) return false;
971
972     // update drop-down for any columns with entry type of N
973
974     for (i = 0; i < this.grid.columns.length; i++) {
975       spec=this.grid.columns[i].format;
976       if (!spec || !spec.EntryType || !spec.FieldName) continue;
977       if (spec.EntryType.charAt(0) != 'N') continue;
978       var SelObj=Rico.$(spec.FieldName);
979       if (!SelObj || SelObj.value!=this.options.TableSelectNew) continue;
980       var newtext=Rico.$("textnew__" + SelObj.id).value;
981       this.addSelectOption(SelObj,newtext,newtext);
982     }
983     
984     // set date values to ISO format
985     for (i = 0; i < dateValues.length; i++) {
986       dateValues[i].e.value = Rico.formatDate(dateValues[i].v,'yyyy-mm-dd');
987     }
988
989     if (typeof tinyMCE!='undefined') tinyMCE.triggerSave();
990     this.makeFormInvisible();
991     this.sendForm();
992     this.menu.cancelmenu();
993     return false;
994   },
995   
996   sendForm: function() {
997     this.setReadOnly("reset");  // reset disabled flag so that all fields are sent to server
998     this.showResponse();
999     Rico.log("sendForm: "+this.grid.tableId);
1000     Rico.ajaxSubmit(this.form, this.options.updateURL, {method:'post',onComplete:this.responseHandler});
1001   }
1002 };
1003
1004
1005 /**
1006  * @namespace Registers custom popup widgets to fill in a text box (e.g. ricoCalendar and ricoTree)
1007  * <pre>
1008  * Custom widget must implement:
1009  *   open() method (make control visible)
1010  *   close() method (hide control)
1011  *   container property (div element that contains the control)
1012  *   id property (uniquely identifies the widget class)
1013  *
1014  * widget calls returnValue method to return a value to the caller
1015  *
1016  * this object handles clicks on the control's icon and positions the control appropriately.
1017  * </pre>
1018  */
1019 Rico.EditControls = {
1020   widgetList : {},
1021   elemList   : {},
1022   zIndex     : 0,
1023
1024   register: function(widget, imgsrc) {
1025     this.widgetList[widget.id] = {imgsrc:imgsrc, widget:widget, currentEl:''};
1026     var self=this;
1027     widget.returnValue=function(newVal,newDesc) { self.setValue(widget,newVal,newDesc); };
1028     Rico.log("Rico.EditControls.register:"+widget.id);
1029   },
1030   
1031   setZ: function(z) {
1032     this.zIndex=Math.max(this.zIndex,z+10);
1033   },
1034
1035   applyTo: function(column,inputCtl,showInput) {
1036     var wInfo=this.widgetList[column.format.SelectCtl];
1037     if (!wInfo) return;
1038     Rico.log('Rico.EditControls.applyTo: '+column.displayName+' : '+column.format.SelectCtl);
1039     var newimg, descSpan = document.createElement('span');
1040     if (wInfo.imgsrc.indexOf('.')==-1 && wInfo.imgsrc.indexOf('/')==-1) {
1041       // treat imgsrc as a class name
1042       newimg = document.createElement('span');
1043       newimg.className=wInfo.imgsrc;
1044     } else {
1045       // treat imgsrc as an image uri
1046       newimg = document.createElement('img');
1047       newimg.src=wInfo.imgsrc;
1048     }
1049     newimg.style.verticalAlign='top';
1050     newimg.style.marginLeft='4px';
1051     newimg.style.cursor='pointer';
1052     newimg.id=this.imgId(column.format.FieldName);
1053     Rico.eventBind(newimg,"click", Rico.eventHandle(this,'processClick'));
1054     inputCtl.parentNode.appendChild(descSpan);
1055     inputCtl.parentNode.appendChild(newimg);
1056     if (showInput) {
1057       descSpan.style.display='none'; 
1058     } else {
1059       inputCtl.style.display='none';    // comment out this line for debugging
1060     }
1061     var clr;
1062     if (column.format.isNullable) {
1063       clr=Rico.clearButton(Rico.eventHandle(this,'processClear'));
1064       clr.id=newimg.id+'_clear';
1065       inputCtl.parentNode.appendChild(clr);
1066     }
1067     this.elemList[newimg.id] = {descSpan:descSpan, inputCtl:inputCtl, widget:wInfo.widget, listObj:wInfo, column:column, clrimg:clr};
1068     column.format.selectIcon=newimg;
1069     column.format.selectDesc=descSpan;
1070   },
1071
1072   displayClrImg: function(column,bShow) {
1073     var el=this.elemList[this.imgId(column.format.FieldName)];
1074     //alert(column.format.FieldName+': '+bShow+' '+el.clrimg.id);
1075     if (el && el.clrimg) el.clrimg.style.display=bShow ? 'inline-block' : 'none';
1076   },
1077
1078   processClear: function(e) {
1079     var elem=Rico.eventElement(e);
1080     var el=this.elemList[elem.id.slice(0,-6)];
1081     if (!el) return;
1082     el.inputCtl.value='';
1083     el.descSpan.innerHTML=el.column._format('');
1084   },
1085
1086   processClick: function(e) {
1087     var elem=Rico.eventElement(e);
1088     var el=this.elemList[elem.id];
1089     if (!el) return;
1090     if (el.listObj.currentEl==elem.id && el.widget.container.style.display!='none') {
1091       el.widget.close();
1092       el.listObj.currentEl='';
1093     } else {
1094       el.listObj.currentEl=elem.id;
1095       Rico.log('Rico.EditControls.processClick: '+el.widget.id+' : '+el.inputCtl.value);
1096       el.widget.container.style.zIndex=this.zIndex;
1097       el.widget.open(el.inputCtl.value,el.column);     // this may change the size of the widget
1098       Rico.positionCtlOverIcon(el.widget.container,elem);
1099     }
1100   },
1101
1102   imgId: function(fieldname) {
1103     return 'icon_'+fieldname;
1104   },
1105
1106   resetValue: function(column) {
1107     var el=this.elemList[this.imgId(column.format.FieldName)];
1108     if (!el) return;
1109     el.inputCtl.value=column.format.ColData;
1110     var v=column._format(column.format.ColData);
1111     if (v==='') v='&nbsp;';
1112     el.descSpan.innerHTML=v;
1113   },
1114
1115   setValue: function(widget,newVal,newDesc) {
1116     var wInfo=this.widgetList[widget.id];
1117     if (!wInfo) return null;
1118     var id=wInfo.currentEl;
1119     if (!id) return null;
1120     var el=this.elemList[id];
1121     if (!el) return null;
1122     el.inputCtl.value=newVal;
1123     if (!newDesc) newDesc=el.column._format(newVal);
1124     el.descSpan.innerHTML=newDesc;
1125     if (el.column.format.DescriptionField)
1126       Rico.$(el.column.format.DescriptionField).value = newDesc;
1127     //alert(widget.id+':'+id+':'+el.inputCtl.id+':'+el.inputCtl.value+':'+newDesc);
1128   },
1129
1130   close: function(id) {
1131     var wInfo=this.widgetList[id];
1132     if (!wInfo) return;
1133     if (wInfo.widget.container.style.display!='none')
1134       wInfo.widget.close();
1135   }
1136 };