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