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