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