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