f005f4dd79bfc6f28cb4fb529a8633803e50fc0b
[infodrom/hallinta] / lib / rico / ricoLiveGrid.js
1 /*
2  *  (c) 2005-2009 Richard Cowin (http://openrico.org)
3  *  (c) 2005-2009 Matt Brown (http://dowdybrown.com)
4  *
5  *  Rico is 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("LiveGrid requires the Rico JavaScript framework");
17 if(typeof RicoUtil=='undefined') throw("LiveGrid requires the RicoUtil Library");
18 if(typeof RicoTranslate=='undefined') throw("LiveGrid requires the RicoTranslate Library");
19 if(typeof Rico.TableColumn=='undefined') throw("LiveGrid requires ricoGridCommon.js");
20
21
22 /** @namespace */
23 Rico.Buffer = {};
24
25 Rico.Buffer.Base = Class.create(
26 /** @lends Rico.Buffer.Base# */
27 {
28 /**
29  * @class Defines the static buffer class (no AJAX).
30  * Loads buffer with data that already exists in the document as an HTML table or passed via javascript.
31  * Also serves as a base class for AJAX-enabled buffers.
32  * @constructs
33  */
34   initialize: function(dataTable, options) {
35     this.clear();
36     this.updateInProgress = false;
37     this.lastOffset = 0;
38     this.rcvdRowCount = false;  // true if an eof element was included in the last xml response
39     this.foundRowCount = false; // true if an xml response is ever received with eof true
40     this.totalRows = 0;
41     this.rowcntContent = "";
42     this.rcvdOffset = -1;
43     this.options = {
44       fixedHdrRows     : 0,
45       canFilter        : true,  // does buffer object support filtering?
46       isEncoded        : true,  // is the data received via ajax html encoded?
47       acceptAttr       : []     // attributes that can be copied from original/ajax data (e.g. className, style, id)
48     };
49     Object.extend(this.options, options || {});
50     if (dataTable) {
51       this.loadRowsFromTable(dataTable,this.options.fixedHdrRows);
52     } else {
53       this.clear();
54     }
55   },
56
57   registerGrid: function(liveGrid) {
58     this.liveGrid = liveGrid;
59   },
60
61   setTotalRows: function( newTotalRows ) {
62     if (typeof(newTotalRows)!='number') newTotalRows=this.size;
63     if (this.totalRows == newTotalRows) return;
64     this.totalRows = newTotalRows;
65     if (this.liveGrid) {
66       Rico.writeDebugMsg("setTotalRows, newTotalRows="+newTotalRows);
67       if (this.liveGrid.sizeTo=='data') this.liveGrid.resizeWindow();
68       this.liveGrid.updateHeightDiv();
69     }
70   },
71
72   loadRowsFromTable: function(tableElement,firstRow) {
73     var newRows = new Array();
74     var trs = tableElement.getElementsByTagName("tr");
75     for ( var i=firstRow || 0; i < trs.length; i++ ) {
76       var row = new Array();
77       var cells = trs[i].getElementsByTagName("td");
78       for ( var j=0; j < cells.length ; j++ )
79         row[j]=cells[j].innerHTML;
80       newRows.push( row );
81     }
82     this.loadRows(newRows);
83   },
84
85   loadRowsFromArray: function(array2D) {
86     for ( var i=0; i < array2D.length; i++ ) {
87       for ( var j=0; j < array2D[i].length ; j++ ) {
88         array2D[i][j]=array2D[i][j].toString();
89       }
90     }
91     this.loadRows(array2D);
92   },
93
94   loadRows: function(jstable) {
95     this.baseRows = jstable;
96     this.startPos = 0;
97     this.size = this.baseRows.length;
98   },
99
100   dom2jstable: function(rowsElement) {
101     Rico.writeDebugMsg('dom2jstable: encoded='+this.options.isEncoded);
102     var newRows = new Array();
103     var trs = rowsElement.getElementsByTagName("tr");
104     for ( var i=0; i < trs.length; i++ ) {
105       var row = new Array();
106       var cells = trs[i].getElementsByTagName("td");
107       for ( var j=0; j < cells.length ; j++ )
108         row[j]=RicoUtil.getContentAsString(cells[j],this.options.isEncoded);
109       newRows.push( row );
110     }
111     return newRows;
112   },
113
114   dom2jstableAttr: function(rowsElement,firstRow) {
115     var acceptAttr=this.options.acceptAttr;
116     Rico.writeDebugMsg("dom2jstableAttr start, # attr="+acceptAttr.length);
117     var newRows = new Array();
118     var trs = rowsElement.getElementsByTagName("tr");
119     for ( var i=firstRow || 0; i < trs.length; i++ ) {
120       var row = new Array();
121       var cells = trs[i].getElementsByTagName("td");
122       for ( var j=0; j < cells.length ; j++ ) {
123         row[j]={};
124         for (var k=0; k<acceptAttr.length; k++)
125           row[j]['_'+acceptAttr[k]]=cells[j].getAttribute(acceptAttr[k]);
126         if (Prototype.Browser.IE) row[j]._class=cells[j].getAttribute('className');
127       }
128       newRows.push( row );
129     }
130     Rico.writeDebugMsg("dom2jstableAttr end");
131     return newRows;
132   },
133
134   _blankRow: function() {
135     var newRow=[];
136     for (var i=0; i<this.liveGrid.columns.length; i++) {
137       newRow[i]='';
138     }
139     return newRow;
140   },
141
142   deleteRows: function(rowIndex,cnt) {
143     this.baseRows.splice(rowIndex,typeof(cnt)=='number' ? cnt : 1);
144     this.liveGrid.isPartialBlank=true;
145     this.size=this.baseRows.length;
146   },
147
148   insertRow: function(beforeRowIndex) {
149     var r=this._blankRow();
150     this.baseRows.splice(beforeRowIndex,0,r);
151     this.size=this.baseRows.length;
152     this.liveGrid.isPartialBlank=true;
153     if (this.startPos < 0) this.startPos=0;
154     return r;
155   },
156
157   appendRows: function(cnt) {
158     var newRows=[];
159     for (var i=0; i<cnt; i++) {
160       var r=this._blankRow();
161       this.baseRows.push(r);
162       newRows.push(r);
163     }
164     this.size=this.baseRows.length;
165     this.liveGrid.isPartialBlank=true;
166     if (this.startPos < 0) this.startPos=0;
167     return newRows;
168   },
169   
170   sortFunc: function(coltype) {
171     switch (coltype) {
172       case 'number': return this._sortNumeric.bind(this);
173       case 'control':return this._sortControl.bind(this);
174       default:       return this._sortAlpha.bind(this);
175     }
176   },
177
178   sortBuffer: function(colnum) {
179     if (!this.baseRows) {
180       this.delayedSortCol=colnum;
181       return;
182     }
183     this.liveGrid.showMsg(RicoTranslate.getPhraseById("sorting"));
184     this.sortColumn=colnum;
185     var col=this.liveGrid.columns[colnum];
186     this.getValFunc=col._sortfunc;
187     this.baseRows.sort(this.sortFunc(col.format.type));
188     if (col.getSortDirection()=='DESC') this.baseRows.reverse();
189   },
190   
191   _sortAlpha: function(a,b) {
192     var aa = this.sortColumn<a.length ? RicoUtil.getInnerText(a[this.sortColumn]) : '';
193     var bb = this.sortColumn<b.length ? RicoUtil.getInnerText(b[this.sortColumn]) : '';
194     if (aa==bb) return 0;
195     if (aa<bb) return -1;
196     return 1;
197   },
198
199   _sortNumeric: function(a,b) {
200     var aa = this.sortColumn<a.length ? this.nan2zero(RicoUtil.getInnerText(a[this.sortColumn])) : 0;
201     var bb = this.sortColumn<b.length ? this.nan2zero(RicoUtil.getInnerText(b[this.sortColumn])) : 0;
202     return aa-bb;
203   },
204
205   nan2zero: function(n) {
206     if (typeof(n)=='string') n=parseFloat(n);
207     return isNaN(n) || typeof(n)=='undefined' ? 0 : n;
208   },
209   
210   _sortControl: function(a,b) {
211     var aa = this.sortColumn<a.length ? RicoUtil.getInnerText(a[this.sortColumn]) : '';
212     var bb = this.sortColumn<b.length ? RicoUtil.getInnerText(b[this.sortColumn]) : '';
213     if (this.getValFunc) {
214       aa=this.getValFunc(aa);
215       bb=this.getValFunc(bb);
216     }
217     if (aa==bb) return 0;
218     if (aa<bb) return -1;
219     return 1;
220   },
221
222   clear: function() {
223     this.baseRows = [];
224     this.rows = [];
225     this.startPos = -1;
226     this.size = 0;
227     this.windowPos = 0;
228   },
229
230   isInRange: function(position) {
231     var lastRow=Math.min(this.totalRows, position + this.liveGrid.pageSize);
232     return (position >= this.startPos) && (lastRow <= this.endPos()); // && (this.size != 0);
233   },
234
235   endPos: function() {
236     return this.startPos + this.rows.length;
237   },
238
239   fetch: function(offset) {
240     Rico.writeDebugMsg('fetch '+this.liveGrid.tableId+': offset='+offset);
241     this.applyFilters();
242     this.setTotalRows();
243     this.rcvdRowCount = true;
244     this.foundRowCount = true;
245     if (offset < 0) offset=0;
246     this.liveGrid.refreshContents(offset);
247     return;
248   },
249
250   exportAllRows: function(populate,finish) {
251     populate(this.getRows(0,this.totalRows));
252     finish();
253   },
254   
255 /**
256  * @return a 2D array of buffer data representing the rows that are currently visible on the grid
257  */
258   visibleRows: function() {
259     return this.rows.slice(this.windowStart,this.windowEnd);
260   },
261
262   setWindow: function(start, count) {
263     this.windowStart = start - this.startPos;                       // position in the buffer of first visible row
264     this.windowEnd = Math.min(this.windowStart + count,this.size);  // position in the buffer of last visible row containing data+1
265     this.windowPos = start;                                         // position in the dataset of first visible row
266   },
267
268 /**
269  * @return true if bufRow is currently visible in the grid
270  */
271   isVisible: function(bufRow) {
272     return bufRow < this.rows.length && bufRow >= this.windowStart && bufRow < this.windowEnd;
273   },
274   
275 /**
276  * takes a window row index and returns the corresponding buffer row index
277  */
278   bufferRow: function(windowRow) {
279     return this.windowStart+windowRow;
280   },
281
282 /**
283  * @return buffer cell at the specified visible row/col index
284  */
285   getWindowCell: function(windowRow,col) {
286     var bufrow=this.bufferRow(windowRow);
287     return this.isVisible(bufrow) && col < this.rows[bufrow].length ? this.rows[bufrow][col] : null;
288   },
289
290   getWindowAttr: function(windowRow,col) {
291     var bufrow=this.bufferRow(windowRow);
292     return this.attr && this.isVisible(bufrow) && col < this.attr[bufrow].length ? this.attr[bufrow][col] : null;
293   },
294
295   getWindowValue: function(windowRow,col) {
296     return this.getWindowCell(windowRow,col);
297   },
298
299   setWindowValue: function(windowRow,col,newval) {
300     var bufrow=this.bufferRow(windowRow);
301     if (bufrow >= this.windowEnd) return false;
302     return this.setValue(bufrow,col,newval);
303   },
304
305   getCell: function(bufRow,col) {
306     return bufRow < this.size ? this.rows[bufRow][col] : null;
307   },
308
309   getValue: function(bufRow,col) {
310     return this.getCell(bufRow,col);
311   },
312
313   setValue: function(bufRow,col,newval,newstyle) {
314     if (bufRow>=this.size) return false;
315     if (!this.rows[bufRow][col]) this.rows[bufRow][col]={};
316     this.rows[bufRow][col]=newval;
317     if (typeof newstyle=='string') this.rows[bufRow][col]._style=newstyle;
318     this.rows[bufRow][col].modified=true;
319     return true;
320   },
321
322   getRows: function(start, count) {
323     var begPos = start - this.startPos;
324     var endPos = Math.min(begPos + count,this.size);
325     var results = new Array();
326     for ( var i=begPos; i < endPos; i++ ) {
327       results.push(this.rows[i]);
328     }
329     return results;
330   },
331
332   applyFilters: function() {
333     var newRows=[],re=[];
334     var r,c,n,i,showRow,filtercnt;
335     var cols=this.liveGrid.columns;
336     for (n=0,filtercnt=0; n<cols.length; n++) {
337       c=cols[n];
338       if (c.filterType == Rico.TableColumn.UNFILTERED) continue;
339       filtercnt++;
340       if (c.filterOp=='LIKE') re[n]=new RegExp(c.filterValues[0],'i');
341     }
342     Rico.writeDebugMsg('applyFilters: # of filters='+filtercnt);
343     if (filtercnt==0) {
344       this.rows = this.baseRows;
345     } else {
346       for (r=0; r<this.baseRows.length; r++) {
347         showRow=true;
348         for (n=0; n<cols.length && showRow; n++) {
349           c=cols[n];
350           if (c.filterType == Rico.TableColumn.UNFILTERED) continue;
351           switch (c.filterOp) {
352             case 'LIKE':
353               showRow=re[n].test(this.baseRows[r][n]);
354               break;
355             case 'EQ':
356               showRow=this.baseRows[r][n]==c.filterValues[0];
357               break;
358             case 'NE':
359               for (i=0; i<c.filterValues.length && showRow; i++)
360                 showRow=this.baseRows[r][n]!=c.filterValues[i];
361               break;
362             case 'LE':
363               if (c.format.type=='number')
364                 showRow=this.nan2zero(this.baseRows[r][n])<=this.nan2zero(c.filterValues[0]);
365               else
366                 showRow=this.baseRows[r][n]<=c.filterValues[0];
367               break;
368             case 'GE':
369               if (c.format.type=='number')
370                 showRow=this.nan2zero(this.baseRows[r][n])>=this.nan2zero(c.filterValues[0]);
371               else
372                 showRow=this.baseRows[r][n]>=c.filterValues[0];
373               break;
374             case 'NULL':
375               showRow=this.baseRows[r][n]=='';
376               break;
377             case 'NOTNULL':
378               showRow=this.baseRows[r][n]!='';
379               break;
380           }
381         }
382         if (showRow) newRows.push(this.baseRows[r]);
383       }
384       this.rows = newRows;
385     }
386     this.rowcntContent = this.size = this.rows.length;
387   }
388
389 });
390
391
392 // Rico.LiveGrid -----------------------------------------------------
393
394 Rico.LiveGrid = Class.create(
395 /** 
396  * @lends Rico.LiveGrid#
397  * @property tableId id string for this grid
398  * @property options the options object passed to the constructor extended with defaults
399  * @property buffer the buffer object containing the data for this grid
400  * @property columns array of {@link Rico.TableColumn} objects
401  */
402 {
403 /**
404  * @class Buffered LiveGrid component
405  * @extends Rico.GridCommon
406  * @constructs
407  */
408   initialize: function( tableId, buffer, options ) {
409     Object.extend(this, new Rico.GridCommon);
410     Object.extend(this, new Rico.LiveGridMethods);
411     this.baseInit();
412     this.tableId = tableId;
413     this.buffer = buffer;
414     Rico.setDebugArea(tableId+"_debugmsgs");    // if used, this should be a textarea
415
416     Object.extend(this.options, {
417       visibleRows      : -1,    // -1 or 'window'=size grid to client window; -2 or 'data'=size grid to min(window,data); -3 or 'body'=size so body does not have a scrollbar; -4 or 'parent'=size to parent element (e.g. if grid is inside a div)
418       frozenColumns    : 0,
419       offset           : 0,     // first row to be displayed
420       prefetchBuffer   : true,  // load table on page load?
421       minPageRows      : 2,
422       maxPageRows      : 50,
423       canSortDefault   : true,  // can be overridden in the column specs
424       canFilterDefault : buffer.options.canFilter, // can be overridden in the column specs
425       canHideDefault   : true,  // can be overridden in the column specs
426
427       // highlight & selection parameters
428       highlightElem    : 'none',// what gets highlighted/selected (cursorRow, cursorCell, menuRow, menuCell, selection, or none)
429       highlightSection : 3,     // which section gets highlighted (frozen=1, scrolling=2, all=3, none=0)
430       highlightMethod  : 'class', // outline, class, both (outline is less CPU intensive on the client)
431       highlightClass   : 'ricoLG_selection',
432
433       // export/print parameters
434       maxPrint         : 1000,  // max # of rows that can be printed/exported, 0=disable print/export feature
435
436       // heading parameters
437       headingSort      : 'link', // link: make headings a link that will sort column, hover: make headings a hoverset, none: events on headings are disabled
438       hdrIconsFirst    : true,   // true: put sort & filter icons before header text, false: after
439       sortAscendImg    : 'sort_asc.gif',
440       sortDescendImg   : 'sort_desc.gif',
441       filterImg        : 'filtercol.gif'
442     });
443     // other options:
444     //   sortCol: initial sort column
445
446     this.options.sortHandler = this.sortHandler.bind(this);
447     this.options.filterHandler = this.filterHandler.bind(this);
448     this.options.onRefreshComplete = this.bookmarkHandler.bind(this);
449     this.options.rowOverHandler = this.rowMouseOver.bindAsEventListener(this);
450     this.options.mouseDownHandler = this.selectMouseDown.bindAsEventListener(this);
451     this.options.mouseOverHandler = this.selectMouseOver.bindAsEventListener(this);
452     this.options.mouseUpHandler  = this.selectMouseUp.bindAsEventListener(this);
453     Object.extend(this.options, options || {});
454
455     switch (typeof this.options.visibleRows) {
456       case 'string':
457         this.sizeTo=this.options.visibleRows;
458         switch (this.options.visibleRows) {
459           case 'data':   this.options.visibleRows=-2; break;
460           case 'body':   this.options.visibleRows=-3; break;
461           case 'parent': this.options.visibleRows=-4; break;
462           default:       this.options.visibleRows=-1; break;
463         }
464         break;
465       case 'number':
466         switch (this.options.visibleRows) {
467           case -1: this.sizeTo='window'; break;
468           case -2: this.sizeTo='data'; break;
469           case -3: this.sizeTo='body'; break;
470           case -4: this.sizeTo='parent'; break;
471           default: this.sizeTo='fixed'; break;
472         }
473         break;
474       default:
475         this.sizeTo='window';
476         this.options.visibleRows=-1;
477         break;
478     }
479     this.highlightEnabled=this.options.highlightSection>0;
480     this.pageSize=0;
481     this.createTables();
482     if (this.headerColCnt==0) {
483       alert('ERROR: no columns found in "'+this.tableId+'"');
484       return;
485     }
486     this.createColumnArray('TableColumn');
487     if (this.options.headingSort=='hover')
488       this.createHoverSet();
489
490     this.bookmark=$(this.tableId+"_bookmark");
491     this.sizeDivs();
492     var filterUIrow=this.buffer.options.canFilter ? this.options.FilterLocation : false;
493     if (typeof(filterUIrow)=='number' && filterUIrow<0)
494       filterUIrow=this.addHeadingRow();
495     this.createDataCells(this.options.visibleRows);
496     if (this.pageSize == 0) return;
497     this.buffer.registerGrid(this);
498     if (this.buffer.setBufferSize) this.buffer.setBufferSize(this.pageSize);
499     this.scrollTimeout = null;
500     this.lastScrollPos = 0;
501     this.attachMenuEvents();
502
503     // preload the images...
504     new Image().src = Rico.imgDir+this.options.filterImg;
505     new Image().src = Rico.imgDir+this.options.sortAscendImg;
506     new Image().src = Rico.imgDir+this.options.sortDescendImg;
507     Rico.writeDebugMsg("images preloaded");
508
509     this.setSortUI( this.options.sortCol, this.options.sortDir );
510     this.setImages();
511     if (this.listInvisible().length==this.columns.length)
512       this.columns[0].showColumn();
513     this.sizeDivs();
514     this.scrollDiv.style.display="";
515     if (this.buffer.totalRows>0)
516       this.updateHeightDiv();
517     if (this.options.prefetchBuffer) {
518       if (this.bookmark) this.bookmark.innerHTML = RicoTranslate.getPhraseById('bookmarkLoading');
519       if (this.options.canFilterDefault && this.options.getQueryParms)
520         this.checkForFilterParms();
521       this.buffer.fetch(this.options.offset);
522     }
523     if (typeof(filterUIrow)=='number')
524       this.createFilters(filterUIrow);
525     this.scrollEventFunc=this.handleScroll.bindAsEventListener(this);
526     this.wheelEventFunc=this.handleWheel.bindAsEventListener(this);
527     this.wheelEvent=(Prototype.Browser.IE || Prototype.Browser.Opera || Prototype.Browser.WebKit) ? 'mousewheel' : 'DOMMouseScroll';
528     if (this.options.offset && this.options.offset < this.buffer.totalRows)
529       setTimeout(this.scrollToRow.bind(this,this.options.offset),50);  // Safari requires a delay
530     this.pluginScroll();
531     this.setHorizontalScroll();
532     if (this.options.windowResize)
533       setTimeout(this.pluginWindowResize.bind(this),100);
534   }
535 });
536
537 Rico.LiveGridMethods = function() {};
538
539 Rico.LiveGridMethods.prototype = {
540 /** @lends Rico.LiveGrid# */
541
542   createHoverSet: function() {
543     var hdrs=[];
544     for( var c=0; c < this.headerColCnt; c++ ) {
545       if (this.columns[c].sortable) {
546         hdrs.push(this.columns[c].hdrCellDiv);
547       }
548     }
549     this.hoverSet = new Rico.HoverSet(hdrs);
550   },
551
552   checkForFilterParms: function() {
553     var s=window.location.search;
554     if (s.charAt(0)=='?') s=s.substring(1);
555     var pairs = s.split('&');
556     for (var i=0; i<pairs.length; i++) {
557       if (pairs[i].match(/^f\[\d+\]/)) {
558         this.buffer.options.requestParameters.push(pairs[i]);
559       }
560     }
561   },
562
563 /**
564  * set filter on a detail grid that is in a master-detail relationship
565  */
566   setDetailFilter: function(colNumber,filterValue) {
567     var c=this.columns[colNumber];
568     c.format.ColData=filterValue;
569     c.setSystemFilter('EQ',filterValue);
570   },
571
572 /**
573  * Create one table for frozen columns and one for scrolling columns.
574  * Also create div's to contain them.
575  * @returns true on success
576  */
577   createTables: function() {
578     var insertloc,hdrSrc,i;
579     var table = $(this.tableId) || $(this.tableId+'_outerDiv');
580     if (!table) return false;
581     if (table.tagName.toLowerCase()=='table') {
582       var theads=table.getElementsByTagName("thead");
583       if (theads.length == 1) {
584         Rico.writeDebugMsg("createTables: using thead section, id="+this.tableId);
585         if (this.options.PanelNamesOnTabHdr && this.options.panels) {
586           var r=theads[0].insertRow(0);
587           this.insertPanelNames(r, 0, this.options.frozenColumns, 'ricoFrozen');
588           this.insertPanelNames(r, this.options.frozenColumns, this.options.columnSpecs.length);
589         }
590         hdrSrc=theads[0].rows;
591       } else {
592         Rico.writeDebugMsg("createTables: using tbody section, id="+this.tableId);
593         hdrSrc=new Array(table.rows[0]);
594       }
595       insertloc=table;
596     } else if (this.options.columnSpecs.length > 0) {
597       if (!table.id.match(/_outerDiv$/)) insertloc=table;
598       Rico.writeDebugMsg("createTables: inserting at "+table.tagName+", id="+this.tableId);
599     } else {
600       alert("ERROR!\n\nUnable to initialize '"+this.tableId+"'\n\nLiveGrid terminated");
601       return false;
602     }
603
604     this.createDivs();
605     this.scrollTabs = this.createDiv("scrollTabs",this.innerDiv);
606     this.shadowDiv  = this.createDiv("shadow",this.scrollDiv);
607     this.shadowDiv.style.direction='ltr';  // avoid FF bug
608     this.scrollDiv.style.display="none";
609     this.scrollDiv.scrollTop=0;
610     if (this.options.highlightMethod!='class') {
611       this.highlightDiv=[];
612       switch (this.options.highlightElem) {
613         case 'menuRow':
614         case 'cursorRow':
615           this.highlightDiv[0] = this.createDiv("highlight",this.outerDiv);
616           this.highlightDiv[0].style.display="none";
617           break;
618         case 'menuCell':
619         case 'cursorCell':
620           for (i=0; i<2; i++) {
621             this.highlightDiv[i] = this.createDiv("highlight",i==0 ? this.frozenTabs : this.scrollTabs);
622             this.highlightDiv[i].style.display="none";
623             this.highlightDiv[i].id+=i;
624           }
625           break;
626         case 'selection':
627           // create one div for each side of the rectangle
628           var parentDiv=this.options.highlightSection==1 ? this.frozenTabs : this.scrollTabs;
629           for (i=0; i<4; i++) {
630             this.highlightDiv[i] = this.createDiv("highlight",parentDiv);
631             this.highlightDiv[i].style.display="none";
632             this.highlightDiv[i].style.overflow="hidden";
633             this.highlightDiv[i].id+=i;
634             this.highlightDiv[i].style[i % 2==0 ? 'height' : 'width']="0px";
635           }
636           break;
637       }
638     }
639
640     // create new tables
641     for (i=0; i<2; i++) {
642       this.tabs[i] = document.createElement("table");
643       this.tabs[i].className = 'ricoLG_table';
644       this.tabs[i].border=0;
645       this.tabs[i].cellPadding=0;
646       this.tabs[i].cellSpacing=0;
647       this.tabs[i].id = this.tableId+"_tab"+i;
648       this.thead[i]=this.tabs[i].createTHead();
649       this.thead[i].className='ricoLG_top';
650       if (this.tabs[i].tBodies.length==0)
651         this.tbody[i]=this.tabs[i].appendChild(document.createElement("tbody"));
652       else
653         this.tbody[i]=this.tabs[i].tBodies[0];
654       this.tbody[i].className='ricoLG_bottom';
655       this.tbody[i].insertRow(-1);
656     }
657     this.frozenTabs.appendChild(this.tabs[0]);
658     this.scrollTabs.appendChild(this.tabs[1]);
659     if (insertloc) insertloc.parentNode.insertBefore(this.outerDiv,insertloc);
660     if (hdrSrc) {
661       this.headerColCnt = this.getColumnInfo(hdrSrc);
662       this.loadHdrSrc(hdrSrc);
663     } else {
664       this.createHdr(0,0,this.options.frozenColumns);
665       this.createHdr(1,this.options.frozenColumns,this.options.columnSpecs.length);
666       if (this.options.PanelNamesOnTabHdr && this.options.panels) {
667         this.insertPanelNames(this.thead[0].insertRow(0), 0, this.options.frozenColumns);
668         this.insertPanelNames(this.thead[1].insertRow(0), this.options.frozenColumns, this.options.columnSpecs.length);
669       }
670       for (i=0; i<2; i++)
671         this.headerColCnt = this.getColumnInfo(this.thead[i].rows);
672     }
673     for( var c=0; c < this.headerColCnt; c++ )
674       this.tbody[c<this.options.frozenColumns ? 0 : 1].rows[0].insertCell(-1);
675     if (insertloc) table.parentNode.removeChild(table);
676     Rico.writeDebugMsg('createTables end');
677     return true;
678   },
679
680   createDataCells: function(visibleRows) {
681     if (visibleRows < 0) {
682       for (var i=0; i<this.options.minPageRows; i++)
683         this.appendBlankRow();
684       this.sizeDivs();
685       this.autoAppendRows(this.remainingHt());
686     } else {
687       for( var r=0; r < visibleRows; r++ )
688         this.appendBlankRow();
689     }
690     var s=this.options.highlightSection;
691     if (s & 1) this.attachHighlightEvents(this.tbody[0]);
692     if (s & 2) this.attachHighlightEvents(this.tbody[1]);
693   },
694
695 /**
696  * @param colnum column number
697  * @return id string for a filter element
698  */
699   filterId: function(colnum) {
700     return 'RicoFilter_'+this.tableId+'_'+colnum;
701   },
702
703 /**
704  * Create filter elements in heading
705  * Reads this.columns[].filterUI to determine type of filter element for each column (t=text box, s=select list, c=custom)
706  * @param r heading row where filter elements will be placed
707  */
708   createFilters: function(r) {
709     for( var c=0; c < this.headerColCnt; c++ ) {
710       var col=this.columns[c];
711       var fmt=col.format;
712       if (typeof fmt.filterUI!='string') continue;
713       var cell=this.hdrCells[r][c].cell;
714       var field,name=this.filterId(c);
715       var divs=cell.getElementsByTagName('div');
716       // copy text alignment from data cell
717       var align=Element.getStyle(this.cell(0,c),'textAlign');
718       Element.setStyle(divs[1], { textAlign: align });
719       switch (fmt.filterUI.charAt(0)) {
720         case 't':
721           // text field
722           field=RicoUtil.createFormField(divs[1],'input','text',name,name);
723           var size=fmt.filterUI.match(/\d+/);
724           field.maxLength=fmt.Length || 50;
725           field.size=size ? parseInt(size,10) : 10;
726           var clrimg = document.createElement('img');
727           clrimg.style.paddingLeft='4px';
728           clrimg.style.cursor='pointer';
729           clrimg.align='top';
730           clrimg.src=Rico.imgDir+'delete.gif';
731           clrimg.alt=RicoTranslate.getPhraseById('clear');
732           divs[1].appendChild(clrimg);
733           Event.observe(clrimg,'click',col.filterClear.bindAsEventListener(col,field),false);
734           if (col.filterType==Rico.TableColumn.USERFILTER && col.filterOp=='LIKE') {
735             var v=col.filterValues[0];
736             if (v.charAt(0)=='*') v=v.substr(1);
737             if (v.slice(-1)=='*') v=v.slice(0,-1);
738             field.value=v;
739             col.lastKeyFilter=v;
740           }
741           Event.observe(field,'keyup',col.filterKeypress.bindAsEventListener(col),false);
742           Event.observe(field,'change',col.filterKeypress.bindAsEventListener(col),false);
743           break;
744         case 's':
745           // drop-down select
746           field=RicoUtil.createFormField(divs[1],'select',null,name);
747           RicoUtil.addSelectOption(field,this.options.FilterAllToken,RicoTranslate.getPhraseById("filterAll"));
748           var options={};
749           Object.extend(options, this.buffer.ajaxOptions);
750           var colnum=typeof(fmt.filterCol)=='number' ? fmt.filterCol : c;
751           options.parameters = 'id='+this.tableId+'&distinct='+colnum;
752           options.onComplete = this.filterValuesUpdate.bind(this,c);
753           new Ajax.Request(this.buffer.dataSource, options);
754           break;
755         case 'c':
756           // custom
757           if (typeof col._createFilters == 'function')
758             col._createFilters(divs[1], name);
759           break;
760       }
761     }
762     this.initFilterImage(r);
763   },
764
765 /**
766  * update select list filter with values in AJAX response
767  * @returns true on success
768  */
769   filterValuesUpdate: function(colnum,request) {
770     var response = request.responseXML.getElementsByTagName("ajax-response");
771     Rico.writeDebugMsg("filterValuesUpdate: "+request.status);
772     if (response == null || response.length != 1) return false;
773     response=response[0];
774     var error = response.getElementsByTagName('error');
775     if (error.length > 0) {
776       Rico.writeDebugMsg("Data provider returned an error:\n"+RicoUtil.getContentAsString(error[0],this.buffer.isEncoded));
777       alert(RicoTranslate.getPhraseById("requestError",RicoUtil.getContentAsString(error[0],this.buffer.isEncoded)));
778       return false;
779     }
780     response=response.getElementsByTagName('response')[0];
781     var rowsElement = response.getElementsByTagName('rows')[0];
782     //var colnum = rowsElement.getAttribute("distinct");
783     var col=this.columns[parseInt(colnum,10)];
784     var rows = this.buffer.dom2jstable(rowsElement);
785     var c0,c1,opt,v, field=$(this.filterId(colnum));
786     if (col.filterType==Rico.TableColumn.USERFILTER && col.filterOp=='EQ') v=col.filterValues[0];
787     Rico.writeDebugMsg('filterValuesUpdate: col='+colnum+' rows='+rows.length);
788     for (var i=0; i<rows.length; i++) {
789       if (rows[i].length>0) {
790         c0=c1=rows[i][0];
791         if (c0.match(/<span\s+class=(['"]?)ricolookup\1>(.*)<\/span>/i)) {
792           c1=RegExp.leftContext;
793         }
794         if (col._getdesc) c1 = col._getdesc(c1);
795         opt=RicoUtil.addSelectOption(field,c0,c1 || RicoTranslate.getPhraseById("filterBlank"));
796         if (col.filterType==Rico.TableColumn.USERFILTER && c0==v) opt.selected=true;
797       }
798     }
799     Event.observe(field,'change',col.filterChange.bindAsEventListener(col),false);
800     return true;
801   },
802
803   unplugHighlightEvents: function() {
804     var s=this.options.highlightSection;
805     if (s & 1) this.detachHighlightEvents(this.tbody[0]);
806     if (s & 2) this.detachHighlightEvents(this.tbody[1]);
807   },
808
809 /**
810  * place panel names on first row of grid header (used by LiveGridForms)
811  */
812   insertPanelNames: function(r,start,limit,cellClass) {
813     Rico.writeDebugMsg('insertPanelNames: start='+start+' limit='+limit);
814     r.className='ricoLG_hdg';
815     var lastIdx=-1, span, newCell=null, spanIdx=0;
816     for( var c=start; c < limit; c++ ) {
817       if (lastIdx == this.options.columnSpecs[c].panelIdx) {
818         span++;
819       } else {
820         if (newCell) newCell.colSpan=span;
821         newCell = r.insertCell(-1);
822         if (cellClass) newCell.className=cellClass;
823         span=1;
824         lastIdx=this.options.columnSpecs[c].panelIdx;
825         newCell.innerHTML=this.options.panels[lastIdx];
826       }
827     }
828     if (newCell) newCell.colSpan=span;
829   },
830
831 /**
832  * create grid header for table i (if none was provided)
833  */
834   createHdr: function(i,start,limit) {
835     Rico.writeDebugMsg('createHdr: i='+i+' start='+start+' limit='+limit);
836     var mainRow = this.thead[i].insertRow(-1);
837     mainRow.id=this.tableId+'_tab'+i+'h_main';
838     mainRow.className='ricoLG_hdg';
839     for( var c=start; c < limit; c++ ) {
840       var newCell = mainRow.insertCell(-1);
841       newCell.innerHTML=this.options.columnSpecs[c].Hdg;
842     }
843   },
844
845 /**
846  * move header cells in original table to grid
847  */
848   loadHdrSrc: function(hdrSrc) {
849     var i,h,c,r,newrow,cells;
850     Rico.writeDebugMsg('loadHdrSrc start');
851     for (i=0; i<2; i++) {
852       for (r=0; r<hdrSrc.length; r++) {
853         newrow = this.thead[i].insertRow(-1);
854         newrow.className='ricoLG_hdg '+this.tableId+'_hdg'+r;
855       }
856     }
857     if (hdrSrc.length==1) {
858       cells=hdrSrc[0].cells;
859       for (c=0; cells.length > 0; c++)
860         this.thead[c<this.options.frozenColumns ? 0 : 1].rows[0].appendChild(cells[0]);
861     } else {
862       for (r=0; r<hdrSrc.length; r++) {
863         cells=hdrSrc[r].cells;
864         for (c=0,h=0; cells.length > 0; c++) {
865           if (cells[0].className=='ricoFrozen') {
866             if (r==this.headerRowIdx) this.options.frozenColumns=c+1;
867           } else {
868             h=1;
869           }
870           this.thead[h].rows[r].appendChild(cells[0]);
871         }
872       }
873     }
874     Rico.writeDebugMsg('loadHdrSrc end');
875   },
876
877 /**
878  * Size div elements
879  */
880   sizeDivs: function() {
881     Rico.writeDebugMsg('sizeDivs: '+this.tableId);
882     //this.cancelMenu();
883     this.unhighlight();
884     this.baseSizeDivs();
885     var firstVisible=this.firstVisible();
886     if (this.pageSize == 0 || firstVisible < 0) return;
887     var totRowHt=this.columns[firstVisible].dataColDiv.offsetHeight;
888     this.rowHeight = Math.round(totRowHt/this.pageSize);
889     var scrHt=this.dataHt;
890     if (this.scrWi>0 || Prototype.Browser.IE || Prototype.Browser.WebKit)
891       scrHt+=this.options.scrollBarWidth;
892     this.scrollDiv.style.height=scrHt+'px';
893     this.innerDiv.style.width=(this.scrWi-this.options.scrollBarWidth+1)+'px';
894     this.resizeDiv.style.height=this.frozenTabs.style.height=this.innerDiv.style.height=(this.hdrHt+this.dataHt+1)+'px';
895     Rico.writeDebugMsg('sizeDivs scrHt='+scrHt+' innerHt='+this.innerDiv.style.height+' rowHt='+this.rowHeight+' pageSize='+this.pageSize);
896     var pad=(this.scrWi-this.scrTabWi < this.options.scrollBarWidth) ? 2 : 0;
897     this.shadowDiv.style.width=(this.scrTabWi+pad)+'px';
898     this.outerDiv.style.height=(this.hdrHt+scrHt)+'px';
899     this.setHorizontalScroll();
900   },
901
902   setHorizontalScroll: function() {
903     var scrleft=this.scrollDiv.scrollLeft;
904     this.scrollTabs.style.left=(-scrleft)+'px';
905   },
906
907   remainingHt: function() {
908     var tabHt;
909     var winHt=RicoUtil.windowHeight();
910     var margin=Prototype.Browser.IE ? 15 : 10;
911     // if there is a horizontal scrollbar take it into account
912     if (!Prototype.Browser.IE && window.frameElement && window.frameElement.scrolling=='yes' && this.sizeTo!='parent') margin+=this.options.scrollBarWidth;
913     switch (this.sizeTo) {
914       case 'window':
915       case 'data':
916         var divPos=Position.page(this.outerDiv);
917         tabHt=Math.max(this.tabs[0].offsetHeight,this.tabs[1].offsetHeight);
918         Rico.writeDebugMsg("remainingHt, winHt="+winHt+' tabHt='+tabHt+' gridY='+divPos[1]);
919         return winHt-divPos[1]-tabHt-this.options.scrollBarWidth-margin;  // allow for scrollbar and some margin
920       case 'parent':
921         var offset=this.offsetFromParent(this.outerDiv);
922         tabHt=Math.max(this.tabs[0].offsetHeight,this.tabs[1].offsetHeight);
923         if (Prototype.Browser.IE) Element.hide(this.outerDiv);
924         var parentHt=this.outerDiv.parentNode.offsetHeight;
925         if (Prototype.Browser.IE) Element.show(this.outerDiv);
926         Rico.writeDebugMsg("remainingHt, parentHt="+parentHt+' gridY='+offset+' winHt='+winHt+' tabHt='+tabHt);
927         return parentHt - tabHt - offset - this.options.scrollBarWidth;
928       case 'body':
929         //Rico.writeDebugMsg("remainingHt, document.height="+document.height);
930         //Rico.writeDebugMsg("remainingHt, body.offsetHeight="+document.body.offsetHeight);
931         //Rico.writeDebugMsg("remainingHt, body.scrollHeight="+document.body.scrollHeight);
932         //Rico.writeDebugMsg("remainingHt, documentElement.scrollHeight="+document.documentElement.scrollHeight);
933         var bodyHt=Prototype.Browser.IE ? document.body.scrollHeight : document.body.offsetHeight;
934         var remHt=winHt-bodyHt-margin;
935         if (!Prototype.Browser.WebKit) remHt-=this.options.scrollBarWidth;
936         Rico.writeDebugMsg("remainingHt, winHt="+winHt+' pageHt='+bodyHt+' remHt='+remHt);
937         return remHt;
938       default:
939         tabHt=Math.max(this.tabs[0].offsetHeight,this.tabs[1].offsetHeight);
940         Rico.writeDebugMsg("remainingHt, winHt="+winHt+' tabHt='+tabHt);
941         if (this.sizeTo.slice(-1)=='%') winHt*=parseFloat(this.sizeTo)/100.0;
942         else if (this.sizeTo.slice(-2)=='px') winHt=parseInt(this.sizeTo,10);
943         return winHt-tabHt-this.options.scrollBarWidth-margin;  // allow for scrollbar and some margin
944     }
945   },
946
947   offsetFromParent: function(element) {
948     var valueT = 0;
949     var elParent=element.parentNode;
950     do {
951       //Rico.writeDebugMsg("offsetFromParent: "+element.tagName+' id='+element.id+' otop='+element.offsetTop);
952       valueT += element.offsetTop  || 0;
953       element = element.offsetParent;
954       if (!element || element==null) break;
955       var p = Element.getStyle(element, 'position');
956       if (element.tagName=='BODY' || element.tagName=='HTML' || p=='absolute') return valueT-elParent.offsetTop;
957     } while (element != elParent);
958     return valueT;
959   },
960
961   adjustPageSize: function() {
962     var remHt=this.remainingHt();
963     Rico.writeDebugMsg('adjustPageSize remHt='+remHt+' lastRow='+this.lastRowPos);
964     if (remHt > this.rowHeight)
965       this.autoAppendRows(remHt);
966     else if (remHt < 0 || this.sizeTo=='data')
967       this.autoRemoveRows(-remHt);
968   },
969
970   pluginWindowResize: function() {
971     this.resizeWindowHandler=this.resizeWindow.bindAsEventListener(this);
972     Event.observe(window, "resize", this.resizeWindowHandler, false);
973   },
974
975   unplugWindowResize: function() {
976     if (!this.resizeWindowHandler) return;
977     Event.stopObserving(window,"resize", this.resizeWindowHandler, false);
978     this.resizeWindowHandler=null;
979   },
980
981   resizeWindow: function() {
982     Rico.writeDebugMsg('resizeWindow '+this.tableId+' lastRow='+this.lastRowPos);
983     if (this.resizeState=='finish') {
984       Rico.writeDebugMsg('resizeWindow postponed');
985       this.resizeState='resize';
986       return;
987     }
988     if (!this.sizeTo || this.sizeTo=='fixed') {
989       this.sizeDivs();
990       return;
991     }
992     if (this.sizeTo=='parent' && Element.getStyle(this.outerDiv.parentNode,'display') == 'none') return;
993     var oldSize=this.pageSize;
994     this.adjustPageSize();
995     if (this.pageSize > oldSize && this.buffer.totalRows>0) {
996       this.isPartialBlank=true;
997       var adjStart=this.adjustRow(this.lastRowPos);
998       this.buffer.fetch(adjStart);
999     } else if (this.pageSize < oldSize) {
1000       if (this.options.onRefreshComplete) this.options.onRefreshComplete(this.contentStartPos,this.contentStartPos+this.pageSize-1);  // update bookmark
1001     }
1002     this.resizeState='finish';
1003     setTimeout(this.finishResize.bind(this),20);
1004     Rico.writeDebugMsg('resizeWindow '+this.tableId+' complete. old size='+oldSize+' new size='+this.pageSize);
1005   },
1006
1007   finishResize: function() {
1008     this.sizeDivs();
1009     this.updateHeightDiv();
1010     if (this.resizeState=='resize') {
1011       this.resizeWindow();
1012     } else {
1013       this.resizeState='';
1014     }
1015   },
1016
1017   topOfLastPage: function() {
1018     return Math.max(this.buffer.totalRows-this.pageSize,0);
1019   },
1020
1021   updateHeightDiv: function() {
1022     var notdisp=this.topOfLastPage();
1023     var ht = this.scrollDiv.clientHeight + this.rowHeight * notdisp;
1024     Rico.writeDebugMsg("updateHeightDiv, ht="+ht+' scrollDiv.clientHeight='+this.scrollDiv.clientHeight+' rowsNotDisplayed='+notdisp);
1025     this.shadowDiv.style.height=ht+'px';
1026   },
1027
1028   autoRemoveRows: function(overage) {
1029     if (!this.rowHeight) return;
1030     var removeCnt=Math.ceil(overage / this.rowHeight);
1031     if (this.sizeTo=='data')
1032       removeCnt=Math.max(removeCnt,this.pageSize-this.buffer.totalRows);
1033     Rico.writeDebugMsg("autoRemoveRows overage="+overage+" removeCnt="+removeCnt);
1034     for (var i=0; i<removeCnt; i++)
1035       this.removeRow();
1036   },
1037
1038   removeRow: function() {
1039     if (this.pageSize <= this.options.minPageRows) return;
1040     this.pageSize--;
1041     for( var c=0; c < this.headerColCnt; c++ ) {
1042       var cell=this.columns[c].cell(this.pageSize);
1043       this.columns[c].dataColDiv.removeChild(cell);
1044     }
1045   },
1046
1047   autoAppendRows: function(overage) {
1048     if (!this.rowHeight) return;
1049     var addCnt=Math.floor(overage / this.rowHeight);
1050     Rico.writeDebugMsg("autoAppendRows overage="+overage+" cnt="+addCnt+" rowHt="+this.rowHeight);
1051     for (var i=0; i<addCnt; i++) {
1052       if (this.sizeTo=='data' && this.pageSize>=this.buffer.totalRows) break;
1053       this.appendBlankRow();
1054     }
1055   },
1056
1057 /**
1058  * on older systems, this can be fairly slow
1059  */
1060   appendBlankRow: function() {
1061     if (this.pageSize >= this.options.maxPageRows) return;
1062     Rico.writeDebugMsg("appendBlankRow #"+this.pageSize);
1063     var cls=this.defaultRowClass(this.pageSize);
1064     for( var c=0; c < this.headerColCnt; c++ ) {
1065       var newdiv = document.createElement("div");
1066       newdiv.className = 'ricoLG_cell '+cls;
1067       newdiv.id=this.tableId+'_'+this.pageSize+'_'+c;
1068       this.columns[c].dataColDiv.appendChild(newdiv);
1069       newdiv.innerHTML='&nbsp;';
1070       if (this.columns[c].format.canDrag && dndMgr)
1071         dndMgr.registerDraggable( new Rico.LiveGridDraggable(this, this.pageSize, c) );
1072       if (this.columns[c]._create)
1073         this.columns[c]._create(newdiv,this.pageSize);
1074     }
1075     this.pageSize++;
1076   },
1077
1078   defaultRowClass: function(rownum) {
1079     return (rownum % 2==0) ? 'ricoLG_evenRow' : 'ricoLG_oddRow';
1080   },
1081
1082   handleMenuClick: function(e) {
1083     if (!this.menu) return;
1084     this.cancelMenu();
1085     this.unhighlight(); // in case highlighting was invoked externally
1086     var idx;
1087     var cell=Event.element(e);
1088     if (cell.className=='ricoLG_highlightDiv') {
1089       idx=this.highlightIdx;
1090     } else {
1091       cell=RicoUtil.getParentByTagName(cell,'div','ricoLG_cell');
1092       if (!cell) return;
1093       idx=this.winCellIndex(cell);
1094       if ((this.options.highlightSection & (idx.tabIdx+1))==0) return;
1095     }
1096     this.highlight(idx);
1097     this.highlightEnabled=false;
1098     if (this.hideScroll) this.scrollDiv.style.overflow="hidden";
1099     this.menuIdx=idx;
1100     if (!this.menu.div) this.menu.createDiv();
1101     this.menu.liveGrid=this;
1102     if (this.menu.buildGridMenu) {
1103       var showMenu=this.menu.buildGridMenu(idx.row, idx.column, idx.tabIdx);
1104       if (!showMenu) return;
1105     }
1106     if (this.options.highlightElem=='selection' && !this.isSelected(idx.cell)) {
1107       this.selectCell(idx.cell);
1108     }
1109     this.menu.showmenu(e,this.closeMenu.bind(this));
1110   },
1111
1112   closeMenu: function() {
1113     if (!this.menuIdx) return;
1114     if (this.hideScroll) this.scrollDiv.style.overflow="";
1115     this.unhighlight();
1116     this.highlightEnabled=true;
1117     this.menuIdx=null;
1118   },
1119
1120 /**
1121  * @return index of cell within the window
1122  */
1123   winCellIndex: function(cell) {
1124     var a=cell.id.split(/_/);
1125     var l=a.length;
1126     var r=parseInt(a[l-2],10);
1127     var c=parseInt(a[l-1],10);
1128     return {row:r, column:c, tabIdx:this.columns[c].tabIdx, cell:cell};
1129   },
1130
1131 /**
1132  * @return index of cell within the dataset
1133  */
1134   datasetIndex: function(cell) {
1135     var idx=this.winCellIndex(cell);
1136     idx.row+=this.buffer.windowPos;
1137     idx.onBlankRow=(idx.row >= this.buffer.endPos());
1138     return idx;
1139   },
1140
1141   attachHighlightEvents: function(tBody) {
1142     switch (this.options.highlightElem) {
1143       case 'selection':
1144         Event.observe(tBody,"mousedown", this.options.mouseDownHandler, false);
1145         /** @ignore */
1146         tBody.ondrag = function () { return false; };
1147         /** @ignore */
1148         tBody.onselectstart = function () { return false; };
1149         break;
1150       case 'cursorRow':
1151       case 'cursorCell':
1152         Event.observe(tBody,"mouseover", this.options.rowOverHandler, false);
1153         break;
1154     }
1155   },
1156
1157   detachHighlightEvents: function(tBody) {
1158     switch (this.options.highlightElem) {
1159       case 'selection':
1160         Event.stopObserving(tBody,"mousedown", this.options.mouseDownHandler, false);
1161         tBody.ondrag = null;
1162         tBody.onselectstart = null;
1163         break;
1164       case 'cursorRow':
1165       case 'cursorCell':
1166         Event.stopObserving(tBody,"mouseover", this.options.rowOverHandler, false);
1167         break;
1168     }
1169   },
1170
1171 /**
1172  * @return array of objects containing row/col indexes (index values are relative to the start of the window)
1173  */
1174   getVisibleSelection: function() {
1175     var cellList=[];
1176     if (this.SelectIdxStart && this.SelectIdxEnd) {
1177       var r1=Math.max(Math.min(this.SelectIdxEnd.row,this.SelectIdxStart.row)-this.buffer.startPos,this.buffer.windowStart);
1178       var r2=Math.min(Math.max(this.SelectIdxEnd.row,this.SelectIdxStart.row)-this.buffer.startPos,this.buffer.windowEnd-1);
1179       var c1=Math.min(this.SelectIdxEnd.column,this.SelectIdxStart.column);
1180       var c2=Math.max(this.SelectIdxEnd.column,this.SelectIdxStart.column);
1181       //Rico.writeDebugMsg("getVisibleSelection "+r1+','+c1+' to '+r2+','+c2+' ('+this.SelectIdxStart.row+',startPos='+this.buffer.startPos+',windowPos='+this.buffer.windowPos+',windowEnd='+this.buffer.windowEnd+')');
1182       for (var r=r1; r<=r2; r++) {
1183         for (var c=c1; c<=c2; c++)
1184           cellList.push({row:r-this.buffer.windowStart,column:c});
1185       }
1186     }
1187     if (this.SelectCtrl) {
1188       for (var i=0; i<this.SelectCtrl.length; i++) {
1189         if (this.SelectCtrl[i].row>=this.buffer.windowStart && this.SelectCtrl[i].row<this.buffer.windowEnd)
1190           cellList.push({row:this.SelectCtrl[i].row-this.buffer.windowStart,column:this.SelectCtrl[i].column});
1191       }
1192     }
1193     return cellList;
1194   },
1195
1196   updateSelectOutline: function() {
1197     if (!this.SelectIdxStart || !this.SelectIdxEnd) return;
1198     var r1=Math.max(Math.min(this.SelectIdxEnd.row,this.SelectIdxStart.row), this.buffer.windowStart);
1199     var r2=Math.min(Math.max(this.SelectIdxEnd.row,this.SelectIdxStart.row), this.buffer.windowEnd-1);
1200     if (r1 > r2) {
1201       this.HideSelection();
1202       return;
1203     }
1204     var c1=Math.min(this.SelectIdxEnd.column,this.SelectIdxStart.column);
1205     var c2=Math.max(this.SelectIdxEnd.column,this.SelectIdxStart.column);
1206     var top1=this.columns[c1].cell(r1-this.buffer.windowStart).offsetTop;
1207     var cell2=this.columns[c1].cell(r2-this.buffer.windowStart);
1208     var bottom2=cell2.offsetTop+cell2.offsetHeight;
1209     var left1=this.columns[c1].dataCell.offsetLeft;
1210     var left2=this.columns[c2].dataCell.offsetLeft;
1211     var right2=left2+this.columns[c2].dataCell.offsetWidth;
1212     //window.status='updateSelectOutline: '+r1+' '+r2+' top='+top1+' bot='+bottom2;
1213     this.highlightDiv[0].style.top=this.highlightDiv[3].style.top=this.highlightDiv[1].style.top=(this.hdrHt+top1-1) + 'px';
1214     this.highlightDiv[2].style.top=(this.hdrHt+bottom2-1)+'px';
1215     this.highlightDiv[3].style.left=(left1-2)+'px';
1216     this.highlightDiv[0].style.left=this.highlightDiv[2].style.left=(left1-1)+'px';
1217     this.highlightDiv[1].style.left=(right2-1)+'px';
1218     this.highlightDiv[0].style.width=this.highlightDiv[2].style.width=(right2-left1-1) + 'px';
1219     this.highlightDiv[1].style.height=this.highlightDiv[3].style.height=(bottom2-top1) + 'px';
1220     //this.highlightDiv[0].style.right=this.highlightDiv[2].style.right=this.highlightDiv[1].style.right=()+'px';
1221     //this.highlightDiv[2].style.bottom=this.highlightDiv[3].style.bottom=this.highlightDiv[1].style.bottom=(this.hdrHt+bottom2) + 'px';
1222     for (var i=0; i<4; i++)
1223       this.highlightDiv[i].style.display='';
1224   },
1225
1226   HideSelection: function() {
1227     var i;
1228     if (this.options.highlightMethod!='class') {
1229       for (i=0; i<this.highlightDiv.length; i++)
1230         this.highlightDiv[i].style.display='none';
1231     }
1232     if (this.options.highlightMethod!='outline') {
1233       var cellList=this.getVisibleSelection();
1234       Rico.writeDebugMsg("HideSelection "+cellList.length);
1235       for (i=0; i<cellList.length; i++)
1236         this.unhighlightCell(this.columns[cellList[i].column].cell(cellList[i].row));
1237     }
1238   },
1239
1240   ShowSelection: function() {
1241     if (this.options.highlightMethod!='class')
1242       this.updateSelectOutline();
1243     if (this.options.highlightMethod!='outline') {
1244       var cellList=this.getVisibleSelection();
1245       for (var i=0; i<cellList.length; i++)
1246         this.highlightCell(this.columns[cellList[i].column].cell(cellList[i].row));
1247     }
1248   },
1249
1250   ClearSelection: function() {
1251     Rico.writeDebugMsg("ClearSelection");
1252     this.HideSelection();
1253     this.SelectIdxStart=null;
1254     this.SelectIdxEnd=null;
1255     this.SelectCtrl=[];
1256   },
1257
1258   selectCell: function(cell) {
1259     this.ClearSelection();
1260     this.SelectIdxStart=this.SelectIdxEnd=this.datasetIndex(cell);
1261     this.ShowSelection();
1262   },
1263
1264   AdjustSelection: function(cell) {
1265     var newIdx=this.datasetIndex(cell);
1266     if (this.SelectIdxStart.tabIdx != newIdx.tabIdx) return;
1267     this.HideSelection();
1268     this.SelectIdxEnd=newIdx;
1269     this.ShowSelection();
1270   },
1271
1272   RefreshSelection: function() {
1273     var cellList=this.getVisibleSelection();
1274     for (var i=0; i<cellList.length; i++) {
1275       this.columns[cellList[i].column].displayValue(cellList[i].row);
1276     }
1277   },
1278
1279   FillSelection: function(newVal,newStyle) {
1280     if (this.SelectIdxStart && this.SelectIdxEnd) {
1281       var r1=Math.min(this.SelectIdxEnd.row,this.SelectIdxStart.row);
1282       var r2=Math.max(this.SelectIdxEnd.row,this.SelectIdxStart.row);
1283       var c1=Math.min(this.SelectIdxEnd.column,this.SelectIdxStart.column);
1284       var c2=Math.max(this.SelectIdxEnd.column,this.SelectIdxStart.column);
1285       for (var r=r1; r<=r2; r++) {
1286         for (var c=c1; c<=c2; c++) {
1287           this.buffer.setValue(r,c,newVal,newStyle);
1288         }
1289       }
1290     }
1291     if (this.SelectCtrl) {
1292       for (var i=0; i<this.SelectCtrl.length; i++) {
1293         this.buffer.setValue(this.SelectCtrl[i].row,this.SelectCtrl[i].column,newVal,newStyle);
1294       }
1295     }
1296     this.RefreshSelection();
1297   },
1298
1299 /**
1300  * Process mouse down event
1301  * @param e event object
1302  */
1303   selectMouseDown: function(e) {
1304     if (this.highlightEnabled==false) return true;
1305     this.cancelMenu();
1306     var cell=Event.element(e);
1307     if (!Event.isLeftClick(e)) return true;
1308     cell=RicoUtil.getParentByTagName(cell,'div','ricoLG_cell');
1309     if (!cell) return true;
1310     Event.stop(e);
1311     var newIdx=this.datasetIndex(cell);
1312     if (newIdx.onBlankRow) return true;
1313     Rico.writeDebugMsg("selectMouseDown @"+newIdx.row+','+newIdx.column);
1314     if (e.ctrlKey) {
1315       if (!this.SelectIdxStart || this.options.highlightMethod!='class') return true;
1316       if (!this.isSelected(cell)) {
1317         this.highlightCell(cell);
1318         this.SelectCtrl.push(this.datasetIndex(cell));
1319       } else {
1320         for (var i=0; i<this.SelectCtrl.length; i++) {
1321           if (this.SelectCtrl[i].row==newIdx.row && this.SelectCtrl[i].column==newIdx.column) {
1322             this.unhighlightCell(cell);
1323             this.SelectCtrl.splice(i,1);
1324             break;
1325           }
1326         }
1327       }
1328     } else if (e.shiftKey) {
1329       if (!this.SelectIdxStart) return true;
1330       this.AdjustSelection(cell);
1331     } else {
1332       this.selectCell(cell);
1333       this.pluginSelect();
1334     }
1335     return false;
1336   },
1337
1338   pluginSelect: function() {
1339     if (this.selectPluggedIn) return;
1340     var tBody=this.tbody[this.SelectIdxStart.tabIdx];
1341     Event.observe(tBody,"mouseover", this.options.mouseOverHandler, false);
1342     Event.observe(this.outerDiv,"mouseup",  this.options.mouseUpHandler,  false);
1343     this.selectPluggedIn=true;
1344   },
1345
1346   unplugSelect: function() {
1347     if (!this.selectPluggedIn) return;
1348     var tBody=this.tbody[this.SelectIdxStart.tabIdx];
1349     Event.stopObserving(tBody,"mouseover", this.options.mouseOverHandler , false);
1350     Event.stopObserving(this.outerDiv,"mouseup", this.options.mouseUpHandler , false);
1351     this.selectPluggedIn=false;
1352   },
1353
1354   selectMouseUp: function(e) {
1355     this.unplugSelect();
1356     var cell=Event.element(e);
1357     cell=RicoUtil.getParentByTagName(cell,'div','ricoLG_cell');
1358     if (!cell) return;
1359     if (this.SelectIdxStart && this.SelectIdxEnd)
1360       this.AdjustSelection(cell);
1361     else
1362       this.ClearSelection();
1363   },
1364
1365   selectMouseOver: function(e) {
1366     var cell=Event.element(e);
1367     cell=RicoUtil.getParentByTagName(cell,'div','ricoLG_cell');
1368     if (!cell) return;
1369     this.AdjustSelection(cell);
1370     Event.stop(e);
1371   },
1372
1373   isSelected: function(cell) {
1374     if (this.options.highlightMethod!='outline') return Element.hasClassName(cell,this.options.highlightClass);
1375     if (!this.SelectIdxStart || !this.SelectIdxEnd) return false;
1376     var r1=Math.max(Math.min(this.SelectIdxEnd.row,this.SelectIdxStart.row), this.buffer.windowStart);
1377     var r2=Math.min(Math.max(this.SelectIdxEnd.row,this.SelectIdxStart.row), this.buffer.windowEnd-1);
1378     if (r1 > r2) return false;
1379     var c1=Math.min(this.SelectIdxEnd.column,this.SelectIdxStart.column);
1380     var c2=Math.max(this.SelectIdxEnd.column,this.SelectIdxStart.column);
1381     var curIdx=this.datasetIndex(cell);
1382     return (r1<=curIdx.row && curIdx.row<=r2 && c1<=curIdx.column && curIdx.column<=c2);
1383   },
1384
1385   highlightCell: function(cell) {
1386     Element.addClassName(cell,this.options.highlightClass);
1387   },
1388
1389   unhighlightCell: function(cell) {
1390     if (cell==null) return;
1391     Element.removeClassName(cell,this.options.highlightClass);
1392   },
1393
1394   selectRow: function(r) {
1395     for (var c=0; c<this.columns.length; c++)
1396       this.highlightCell(this.columns[c].cell(r));
1397   },
1398
1399   unselectRow: function(r) {
1400     for (var c=0; c<this.columns.length; c++)
1401       this.unhighlightCell(this.columns[c].cell(r));
1402   },
1403
1404   rowMouseOver: function(e) {
1405     if (!this.highlightEnabled) return;
1406     var cell=Event.element(e);
1407     cell=RicoUtil.getParentByTagName(cell,'div','ricoLG_cell');
1408     if (!cell) return;
1409     var newIdx=this.winCellIndex(cell);
1410     if ((this.options.highlightSection & (newIdx.tabIdx+1))==0) return;
1411     this.highlight(newIdx);
1412   },
1413
1414   highlight: function(newIdx) {
1415     if (this.options.highlightMethod!='outline') this.cursorSetClass(newIdx);
1416     if (this.options.highlightMethod!='class') this.cursorOutline(newIdx);
1417     this.highlightIdx=newIdx;
1418   },
1419
1420   cursorSetClass: function(newIdx) {
1421     switch (this.options.highlightElem) {
1422       case 'menuCell':
1423       case 'cursorCell':
1424         if (this.highlightIdx) this.unhighlightCell(this.highlightIdx.cell);
1425         this.highlightCell(newIdx.cell);
1426         break;
1427       case 'menuRow':
1428       case 'cursorRow':
1429         if (this.highlightIdx) this.unselectRow(this.highlightIdx.row);
1430         var s1=this.options.highlightSection & 1;
1431         var s2=this.options.highlightSection & 2;
1432         var c0=s1 ? 0 : this.options.frozenColumns;
1433         var c1=s2 ? this.columns.length : this.options.frozenColumns;
1434         for (var c=c0; c<c1; c++)
1435           this.highlightCell(this.columns[c].cell(newIdx.row));
1436         break;
1437       default: return;
1438     }
1439   },
1440
1441   cursorOutline: function(newIdx) {
1442     var div;
1443     switch (this.options.highlightElem) {
1444       case 'menuCell':
1445       case 'cursorCell':
1446         div=this.highlightDiv[newIdx.tabIdx];
1447         div.style.left=(this.columns[newIdx.column].dataCell.offsetLeft-1)+'px';
1448         div.style.width=this.columns[newIdx.column].colWidth;
1449         this.highlightDiv[1-newIdx.tabIdx].style.display='none';
1450         break;
1451       case 'menuRow':
1452       case 'cursorRow':
1453         div=this.highlightDiv[0];
1454         var s1=this.options.highlightSection & 1;
1455         var s2=this.options.highlightSection & 2;
1456         div.style.left=s1 ? '0px' : this.frozenTabs.style.width;
1457         div.style.width=((s1 ? this.frozenTabs.offsetWidth : 0) + (s2 ? this.innerDiv.offsetWidth : 0) - 4)+'px';
1458         break;
1459       default: return;
1460     }
1461     div.style.top=(this.hdrHt+newIdx.row*this.rowHeight-1)+'px';
1462     div.style.height=(this.rowHeight-1)+'px';
1463     div.style.display='';
1464   },
1465
1466   unhighlight: function() {
1467     switch (this.options.highlightElem) {
1468       case 'menuCell':
1469         this.highlightIdx=this.menuIdx;
1470         /*jsl:fallthru*/
1471       case 'cursorCell':
1472         if (this.highlightIdx) this.unhighlightCell(this.highlightIdx.cell);
1473         if (!this.highlightDiv) return;
1474         for (var i=0; i<2; i++)
1475           this.highlightDiv[i].style.display='none';
1476         break;
1477       case 'menuRow':
1478         this.highlightIdx=this.menuIdx;
1479         /*jsl:fallthru*/
1480       case 'cursorRow':
1481         if (this.highlightIdx) this.unselectRow(this.highlightIdx.row);
1482         if (this.highlightDiv) this.highlightDiv[0].style.display='none';
1483         break;
1484     }
1485   },
1486
1487   resetContents: function(resetHt) {
1488     Rico.writeDebugMsg("resetContents("+resetHt+")");
1489     this.ClearSelection();
1490     this.buffer.clear();
1491     this.clearRows();
1492     if (typeof resetHt=='undefined' || resetHt==true) {
1493       this.buffer.setTotalRows(0);
1494     } else {
1495       this.scrollToRow(0);
1496     }
1497     this.clearBookmark();
1498   },
1499
1500   setImages: function() {
1501     for (var n=0; n<this.columns.length; n++)
1502       this.columns[n].setImage();
1503   },
1504
1505   // returns column index, or -1 if there are no sorted columns
1506   findSortedColumn: function() {
1507     for (var n=0; n<this.columns.length; n++) {
1508       if (this.columns[n].isSorted()) return n;
1509     }
1510     return -1;
1511   },
1512
1513   findColumnName: function(name) {
1514     for (var n=0; n<this.columns.length; n++) {
1515       if (this.columns[n].fieldName == name) return n;
1516     }
1517     return -1;
1518   },
1519
1520 /**
1521  * Set initial sort
1522  */
1523   setSortUI: function( columnNameOrNum, sortDirection ) {
1524     Rico.writeDebugMsg("setSortUI: "+columnNameOrNum+' '+sortDirection);
1525     var colnum=this.findSortedColumn();
1526     if (colnum >= 0) {
1527       sortDirection=this.columns[colnum].getSortDirection();
1528     } else {
1529       if (typeof sortDirection!='string') {
1530         sortDirection=Rico.TableColumn.SORT_ASC;
1531       } else {
1532         sortDirection=sortDirection.toUpperCase();
1533         if (sortDirection != Rico.TableColumn.SORT_DESC) sortDirection=Rico.TableColumn.SORT_ASC;
1534       }
1535       switch (typeof columnNameOrNum) {
1536         case 'string':
1537           colnum=this.findColumnName(columnNameOrNum);
1538           break;
1539         case 'number':
1540           colnum=columnNameOrNum;
1541           break;
1542       }
1543     }
1544     if (typeof(colnum)!='number' || colnum < 0) return;
1545     this.clearSort();
1546     this.columns[colnum].setSorted(sortDirection);
1547     this.buffer.sortBuffer(colnum);
1548   },
1549
1550 /**
1551  * clear sort flag on all columns
1552  */
1553   clearSort: function() {
1554     for (var x=0;x<this.columns.length;x++)
1555       this.columns[x].setUnsorted();
1556   },
1557
1558 /**
1559  * clear filters on all columns
1560  */
1561   clearFilters: function() {
1562     for (var x=0;x<this.columns.length;x++) {
1563       this.columns[x].setUnfiltered(true);
1564     }
1565     if (this.options.filterHandler) {
1566       this.options.filterHandler();
1567     }
1568   },
1569
1570 /**
1571  * returns number of columns with a user filter set
1572  */
1573   filterCount: function() {
1574     for (var x=0,cnt=0;x<this.columns.length;x++) {
1575       if (this.columns[x].isFiltered()) cnt++;
1576     }
1577     return cnt;
1578   },
1579
1580   sortHandler: function() {
1581     this.cancelMenu();
1582     this.ClearSelection();
1583     this.setImages();
1584     var n=this.findSortedColumn();
1585     if (n < 0) return;
1586     Rico.writeDebugMsg("sortHandler: sorting column "+n);
1587     this.buffer.sortBuffer(n);
1588     this.clearRows();
1589     this.scrollDiv.scrollTop = 0;
1590     this.buffer.fetch(0);
1591   },
1592
1593   filterHandler: function() {
1594     Rico.writeDebugMsg("filterHandler");
1595     this.cancelMenu();
1596     if (this.buffer.processingRequest) {
1597       this.queueFilter=true;
1598       return;
1599     }
1600     this.unplugScroll();
1601     this.ClearSelection();
1602     this.setImages();
1603     this.clearBookmark();
1604     this.clearRows();
1605     this.buffer.fetch(-1);
1606     setTimeout(this.pluginScroll.bind(this), 1); // resetting ht div can cause a scroll event, triggering an extra fetch
1607   },
1608
1609   clearBookmark: function() {
1610     if (this.bookmark) this.bookmark.innerHTML="&nbsp;";
1611   },
1612
1613   bookmarkHandler: function(firstrow,lastrow) {
1614     var newhtml;
1615     if (isNaN(firstrow) || !this.bookmark) return;
1616     var totrows=this.buffer.totalRows;
1617     if (totrows < lastrow) lastrow=totrows;
1618     if (totrows<=0) {
1619       newhtml = RicoTranslate.getPhraseById('bookmarkNoMatch');
1620     } else if (lastrow<0) {
1621       newhtml = RicoTranslate.getPhraseById('bookmarkNoRec');
1622     } else if (this.buffer.foundRowCount) {
1623       newhtml = RicoTranslate.getPhraseById('bookmarkExact',firstrow,lastrow,totrows);
1624     } else {
1625       newhtml = RicoTranslate.getPhraseById('bookmarkAbout',firstrow,lastrow,totrows);
1626     }
1627     this.bookmark.innerHTML = newhtml;
1628   },
1629
1630   clearRows: function() {
1631     if (this.isBlank==true) return;
1632     for (var c=0; c < this.columns.length; c++)
1633       this.columns[c].clearColumn();
1634     this.isBlank = true;
1635   },
1636
1637   refreshContents: function(startPos) {
1638     Rico.writeDebugMsg("refreshContents: startPos="+startPos+" lastRow="+this.lastRowPos+" PartBlank="+this.isPartialBlank+" pageSize="+this.pageSize);
1639     this.hideMsg();
1640     this.cancelMenu();
1641     this.unhighlight(); // in case highlighting was manually invoked
1642     if (this.queueFilter) {
1643       Rico.writeDebugMsg("refreshContents: cancelling refresh because filter has changed");
1644       this.queueFilter=false;
1645       this.filterHandler();
1646       return;
1647     }
1648     this.highlightEnabled=this.options.highlightSection!='none';
1649     if (startPos == this.lastRowPos && !this.isPartialBlank && !this.isBlank) return;
1650     this.isBlank = false;
1651     var viewPrecedesBuffer = this.buffer.startPos > startPos;
1652     var contentStartPos = viewPrecedesBuffer ? this.buffer.startPos: startPos;
1653     this.contentStartPos = contentStartPos+1;
1654     var contentEndPos = Math.min(this.buffer.startPos + this.buffer.size, startPos + this.pageSize);
1655     var onRefreshComplete = this.options.onRefreshComplete;
1656
1657     if ((startPos + this.pageSize < this.buffer.startPos) ||
1658         (this.buffer.startPos + this.buffer.size < startPos) ||
1659         (this.buffer.size == 0)) {
1660       this.clearRows();
1661       if (onRefreshComplete) onRefreshComplete(this.contentStartPos,contentEndPos);  // update bookmark
1662       return;
1663     }
1664
1665     Rico.writeDebugMsg('refreshContents: contentStartPos='+contentStartPos+' contentEndPos='+contentEndPos+' viewPrecedesBuffer='+viewPrecedesBuffer);
1666     var rowSize = contentEndPos - contentStartPos;
1667     this.buffer.setWindow(contentStartPos, rowSize );
1668     var blankSize = this.pageSize - rowSize;
1669     var blankOffset = viewPrecedesBuffer ? 0: rowSize;
1670     var contentOffset = viewPrecedesBuffer ? blankSize: 0;
1671
1672     for (var r=0; r < rowSize; r++) { //initialize what we have
1673       for (var c=0; c < this.columns.length; c++)
1674         this.columns[c].displayValue(r + contentOffset);
1675     }
1676     for (var i=0; i < blankSize; i++)     // blank out the rest
1677       this.blankRow(i + blankOffset);
1678     if (this.options.highlightElem=='selection') this.ShowSelection();
1679     this.isPartialBlank = blankSize > 0;
1680     this.lastRowPos = startPos;
1681     Rico.writeDebugMsg("refreshContents complete, startPos="+startPos);
1682     if (onRefreshComplete) onRefreshComplete(this.contentStartPos,contentEndPos);  // update bookmark
1683   },
1684
1685   scrollToRow: function(rowOffset) {
1686      var p=this.rowToPixel(rowOffset);
1687      Rico.writeDebugMsg("scrollToRow, rowOffset="+rowOffset+" pixel="+p);
1688      this.scrollDiv.scrollTop = p; // this causes a scroll event
1689      if ( this.options.onscroll )
1690         this.options.onscroll( this, rowOffset );
1691   },
1692
1693   scrollUp: function() {
1694      this.moveRelative(-1);
1695   },
1696
1697   scrollDown: function() {
1698      this.moveRelative(1);
1699   },
1700
1701   pageUp: function() {
1702      this.moveRelative(-this.pageSize);
1703   },
1704
1705   pageDown: function() {
1706      this.moveRelative(this.pageSize);
1707   },
1708
1709   adjustRow: function(rowOffset) {
1710      var notdisp=this.topOfLastPage();
1711      if (notdisp == 0 || !rowOffset) return 0;
1712      return Math.min(notdisp,rowOffset);
1713   },
1714
1715   rowToPixel: function(rowOffset) {
1716      return this.adjustRow(rowOffset) * this.rowHeight;
1717   },
1718
1719 /**
1720  * @returns row to display at top of scroll div
1721  */
1722   pixeltorow: function(p) {
1723      var notdisp=this.topOfLastPage();
1724      if (notdisp == 0) return 0;
1725      var prow=parseInt(p/this.rowHeight,10);
1726      return Math.min(notdisp,prow);
1727   },
1728
1729   moveRelative: function(relOffset) {
1730      var newoffset=Math.max(this.scrollDiv.scrollTop+relOffset*this.rowHeight,0);
1731      newoffset=Math.min(newoffset,this.scrollDiv.scrollHeight);
1732      //Rico.writeDebugMsg("moveRelative, newoffset="+newoffset);
1733      this.scrollDiv.scrollTop=newoffset;
1734   },
1735
1736   pluginScroll: function() {
1737      if (this.scrollPluggedIn) return;
1738      Rico.writeDebugMsg("pluginScroll: wheelEvent="+this.wheelEvent);
1739      Event.observe(this.scrollDiv,"scroll",this.scrollEventFunc, false);
1740      for (var t=0; t<2; t++)
1741        Event.observe(this.tabs[t],this.wheelEvent,this.wheelEventFunc, false);
1742      this.scrollPluggedIn=true;
1743   },
1744
1745   unplugScroll: function() {
1746      if (!this.scrollPluggedIn) return;
1747      Rico.writeDebugMsg("unplugScroll");
1748      Event.stopObserving(this.scrollDiv,"scroll", this.scrollEventFunc , false);
1749      for (var t=0; t<2; t++)
1750        Event.stopObserving(this.tabs[t],this.wheelEvent,this.wheelEventFunc, false);
1751      this.scrollPluggedIn=false;
1752   },
1753
1754   handleWheel: function(e) {
1755     var delta = 0;
1756     if (e.wheelDelta) {
1757       if (Prototype.Browser.Opera)
1758         delta = e.wheelDelta/120;
1759       else if (Prototype.Browser.WebKit)
1760         delta = -e.wheelDelta/12;
1761       else
1762         delta = -e.wheelDelta/120;
1763     } else if (e.detail) {
1764       delta = e.detail/3; /* Mozilla/Gecko */
1765     }
1766     if (delta) this.moveRelative(delta);
1767     Event.stop(e);
1768     return false;
1769   },
1770
1771   handleScroll: function(e) {
1772      if ( this.scrollTimeout )
1773        clearTimeout( this.scrollTimeout );
1774      this.setHorizontalScroll();
1775      var scrtop=this.scrollDiv.scrollTop;
1776      var vscrollDiff = this.lastScrollPos-scrtop;
1777      if (vscrollDiff == 0.00) return;
1778      var newrow=this.pixeltorow(scrtop);
1779      if (newrow == this.lastRowPos && !this.isPartialBlank && !this.isBlank) return;
1780      var stamp1 = new Date();
1781      //Rico.writeDebugMsg("handleScroll, newrow="+newrow+" scrtop="+scrtop);
1782      if (this.options.highlightElem=='selection') this.HideSelection();
1783      this.buffer.fetch(newrow);
1784      if (this.options.onscroll) this.options.onscroll(this, newrow);
1785      this.scrollTimeout = setTimeout(this.scrollIdle.bind(this), 1200 );
1786      this.lastScrollPos = this.scrollDiv.scrollTop;
1787      var stamp2 = new Date();
1788      //Rico.writeDebugMsg("handleScroll, time="+(stamp2.getTime()-stamp1.getTime()));
1789   },
1790
1791   scrollIdle: function() {
1792      if ( this.options.onscrollidle )
1793         this.options.onscrollidle();
1794   },
1795
1796   printAll: function(exportType) {
1797     this.showMsg(RicoTranslate.getPhraseById('exportInProgress'));
1798     setTimeout(this._printAll.bind(this,exportType),10);  // allow message to paint
1799   },
1800
1801 /**
1802  * Support function for printAll()
1803  */
1804   _printAll: function(exportType) {
1805     this.exportStart();
1806     this.buffer.exportAllRows(this.exportBuffer.bind(this),this.exportFinish.bind(this,exportType));
1807   },
1808
1809   _printVisible: function(exportType) {
1810     this.exportStart();
1811     this.exportBuffer(this.buffer.visibleRows(),0);
1812     this.exportFinish(exportType);
1813   },
1814
1815 /**
1816  * Send all rows to print/export window
1817  */
1818   exportBuffer: function(rows,startPos) {
1819     var r,c,v,col,exportText;
1820     Rico.writeDebugMsg("exportBuffer: "+rows.length+" rows");
1821     var tdstyle=[];
1822     var totalcnt=startPos || 0;
1823     for (c=0; c<this.columns.length; c++) {
1824       if (this.columns[c].visible) tdstyle[c]=this.exportStyle(this.columns[c].cell(0));  // assumes row 0 style applies to all rows
1825     }
1826     for(r=0; r < rows.length; r++) {
1827       exportText='';
1828       for (c=0; c<this.columns.length; c++) {
1829         if (!this.columns[c].visible) continue;
1830         col=this.columns[c];
1831         col.expStyle=tdstyle[c];
1832         v=col._export(rows[r][c],rows[r]);
1833         if (v.match(/<span\s+class=(['"]?)ricolookup\1>(.*)<\/span>/i))
1834           v=RegExp.leftContext;
1835         if (v=='') v='&nbsp;';
1836         exportText+="<td style='"+col.expStyle+"'>"+v+"</td>";
1837       }
1838       this.exportRows.push(exportText);
1839       totalcnt++;
1840       if (totalcnt % 10 == 0) window.status=RicoTranslate.getPhraseById('exportStatus',totalcnt);
1841     }
1842   }
1843
1844 };
1845
1846
1847 Rico.TableColumn.prototype = 
1848 /** @lends Rico.TableColumn# */
1849 {
1850 /**
1851  * Implements a LiveGrid column. Also contains static properties used by SimpleGrid columns.
1852  * @extends Rico.TableColumnBase
1853  * @constructs
1854  */
1855 initialize: function(liveGrid,colIdx,hdrInfo,tabIdx) {
1856   Object.extend(this, new Rico.TableColumnBase());
1857   this.baseInit(liveGrid,colIdx,hdrInfo,tabIdx);
1858   if (typeof this.format.type!='string') this.format.type='raw';
1859   if (typeof this.isNullable!='boolean') this.isNullable = /number|date/.test(this.format.type);
1860   this.isText = /raw|text|showTags/.test(this.format.type);
1861   Rico.writeDebugMsg(" sortable="+this.sortable+" filterable="+this.filterable+" hideable="+this.hideable+" isNullable="+this.isNullable+' isText='+this.isText);
1862   this.fixHeaders(this.liveGrid.tableId, this.options.hdrIconsFirst);
1863   if (this.format.control) {
1864     // copy all properties/methods that start with '_'
1865     if (typeof this.format.control=='string') {
1866       this.format.control=eval(this.format.control);
1867     }
1868     for (var property in this.format.control) {
1869       if (property.charAt(0)=='_') {
1870         Rico.writeDebugMsg("Copying control property "+property);
1871         this[property] = this.format.control[property];
1872       }
1873     }
1874   }
1875   if (this['format_'+this.format.type])
1876     this._format=this['format_'+this.format.type].bind(this);
1877 },
1878
1879 /**
1880  * Sorts the column in ascending order
1881  */
1882 sortAsc: function() {
1883   this.setColumnSort(Rico.TableColumn.SORT_ASC);
1884 },
1885
1886 /**
1887  * Sorts the column in descending order
1888  */
1889 sortDesc: function() {
1890   this.setColumnSort(Rico.TableColumn.SORT_DESC);
1891 },
1892
1893 /**
1894  * Sorts the column in the specified direction
1895  * @param direction must be one of Rico.TableColumn.UNSORTED, .SORT_ASC, or .SORT_DESC
1896  */
1897 setColumnSort: function(direction) {
1898   this.liveGrid.clearSort();
1899   this.setSorted(direction);
1900   if (this.liveGrid.options.saveColumnInfo.sort)
1901     this.liveGrid.setCookie();
1902   if (this.options.sortHandler)
1903     this.options.sortHandler();
1904 },
1905
1906 /**
1907  * @returns true if this column is allowed to be sorted
1908  */
1909 isSortable: function() {
1910   return this.sortable;
1911 },
1912
1913 /**
1914  * @returns true if this column is currently sorted
1915  */
1916 isSorted: function() {
1917   return this.currentSort != Rico.TableColumn.UNSORTED;
1918 },
1919
1920 /**
1921  * @returns Rico.TableColumn.UNSORTED, .SORT_ASC, or .SORT_DESC
1922  */
1923 getSortDirection: function() {
1924   return this.currentSort;
1925 },
1926
1927 /**
1928  * toggle the sort sequence for this column
1929  */
1930 toggleSort: function() {
1931   if (this.liveGrid.buffer && this.liveGrid.buffer.totalRows==0) return;
1932   if (this.currentSort == Rico.TableColumn.SORT_ASC)
1933     this.sortDesc();
1934   else
1935     this.sortAsc();
1936 },
1937
1938 /**
1939  * Flags that this column is not sorted
1940  */
1941 setUnsorted: function() {
1942   this.setSorted(Rico.TableColumn.UNSORTED);
1943 },
1944
1945 /**
1946  * Flags that this column is sorted, but doesn't actually carry out the sort
1947  * @param direction must be one of Rico.TableColumn.UNSORTED, .SORT_ASC, or .SORT_DESC
1948  */
1949 setSorted: function(direction) {
1950   this.currentSort = direction;
1951 },
1952
1953 /**
1954  * @returns true if this column is allowed to be filtered
1955  */
1956 canFilter: function() {
1957   return this.filterable;
1958 },
1959
1960 /**
1961  * @returns a textual representation of how this column is filtered
1962  */
1963 getFilterText: function() {
1964   var vals=[];
1965   for (var i=0; i<this.filterValues.length; i++) {
1966     var v=this.filterValues[i];
1967     if (typeof(v)=='string' && v.match(/<span\s+class=(['"]?)ricolookup\1>(.*)<\/span>/i)) v=RegExp.leftContext;
1968     vals.push(v=='' ? RicoTranslate.getPhraseById('filterBlank') : v);
1969   }
1970   switch (this.filterOp) {
1971     case 'EQ':   return '= '+(vals[0]);
1972     case 'NE':   return RicoTranslate.getPhraseById('filterNot',vals.join(', '));
1973     case 'LE':   return '<= '+vals[0];
1974     case 'GE':   return '>= '+vals[0];
1975     case 'LIKE': return RicoTranslate.getPhraseById('filterLike',vals[0]);
1976     case 'NULL': return RicoTranslate.getPhraseById('filterEmpty');
1977     case 'NOTNULL': return RicoTranslate.getPhraseById('filterNotEmpty');
1978   }
1979   return '?';
1980 },
1981
1982 /**
1983  * @returns returns the query string representation of the filter
1984  */
1985 getFilterQueryParm: function() {
1986   if (this.filterType == Rico.TableColumn.UNFILTERED) return '';
1987   var retval='&f['+this.index+'][op]='+this.filterOp;
1988   retval+='&f['+this.index+'][len]='+this.filterValues.length;
1989   for (var i=0; i<this.filterValues.length; i++) {
1990     retval+='&f['+this.index+']['+i+']='+escape(this.filterValues[i]);
1991   }
1992   return retval;
1993 },
1994
1995 /**
1996  * removes the filter from this column
1997  */
1998 setUnfiltered: function(skipHandler) {
1999   this.filterType = Rico.TableColumn.UNFILTERED;
2000   if (this.liveGrid.options.saveColumnInfo.filter)
2001     this.liveGrid.setCookie();
2002   if (this.removeFilterFunc)
2003     this.removeFilterFunc();
2004   if (this.options.filterHandler && !skipHandler)
2005     this.options.filterHandler();
2006 },
2007
2008 setFilterEQ: function() {
2009   this.setUserFilter('EQ');
2010 },
2011 setFilterNE: function() {
2012   this.setUserFilter('NE');
2013 },
2014 addFilterNE: function() {
2015   this.filterValues.push(this.userFilter);
2016   if (this.liveGrid.options.saveColumnInfo.filter)
2017     this.liveGrid.setCookie();
2018   if (this.options.filterHandler)
2019     this.options.filterHandler();
2020 },
2021 setFilterGE: function() { this.setUserFilter('GE'); },
2022 setFilterLE: function() { this.setUserFilter('LE'); },
2023 setFilterKW: function(keyword) {
2024   if (keyword!='' && keyword!=null) {
2025     this.setFilter('LIKE',keyword,Rico.TableColumn.USERFILTER);
2026   } else {
2027     this.setUnfiltered(false);
2028   }
2029 },
2030
2031 setUserFilter: function(relop) {
2032   this.setFilter(relop,this.userFilter,Rico.TableColumn.USERFILTER);
2033 },
2034
2035 setSystemFilter: function(relop,filter) {
2036   this.setFilter(relop,filter,Rico.TableColumn.SYSTEMFILTER);
2037 },
2038
2039 setFilter: function(relop,filter,type,removeFilterFunc) {
2040   this.filterValues = [filter];
2041   this.filterType = type;
2042   this.filterOp = relop;
2043   if (type == Rico.TableColumn.USERFILTER && this.liveGrid.options.saveColumnInfo.filter)
2044     this.liveGrid.setCookie();
2045   this.removeFilterFunc=removeFilterFunc;
2046   if (this.options.filterHandler)
2047     this.options.filterHandler();
2048 },
2049
2050 isFiltered: function() {
2051   return this.filterType == Rico.TableColumn.USERFILTER;
2052 },
2053
2054 filterChange: function(e) {
2055   var selbox=Event.element(e);
2056   if (selbox.value==this.liveGrid.options.FilterAllToken)
2057     this.setUnfiltered();
2058   else
2059     this.setFilter('EQ',selbox.value,Rico.TableColumn.USERFILTER,function() {selbox.selectedIndex=0;});
2060 },
2061
2062 filterClear: function(e,txtbox) {
2063   txtbox.value='';
2064   this.setUnfiltered();
2065 },
2066
2067 filterKeypress: function(e) {
2068   var txtbox=Event.element(e);
2069   if (typeof this.lastKeyFilter != 'string') this.lastKeyFilter='';
2070   if (this.lastKeyFilter==txtbox.value) return;
2071   var v=txtbox.value;
2072   Rico.writeDebugMsg("filterKeypress: "+this.index+' '+v);
2073   this.lastKeyFilter=v;
2074   if (v=='' || v=='*')
2075     this.setUnfiltered();
2076   else {
2077     this.setFilter('LIKE', v, Rico.TableColumn.USERFILTER, function() {txtbox.value='';});
2078   }
2079 },
2080
2081 format_text: function(v) {
2082   if (typeof v!='string')
2083     return '&nbsp;';
2084   else
2085     return v.stripTags();
2086 },
2087
2088 format_showTags: function(v) {
2089   if (typeof v!='string')
2090     return '&nbsp;';
2091   else
2092     return v.replace(/&/g, '&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
2093 },
2094
2095 format_number: function(v) {
2096   if (typeof v=='undefined' || v=='' || v==null)
2097     return '&nbsp;';
2098   else
2099     return v.formatNumber(this.format);
2100 },
2101
2102 format_datetime: function(v) {
2103   if (typeof v=='undefined' || v=='' || v==null)
2104     return '&nbsp;';
2105   else {
2106     var d=new Date();
2107     if (!d.setISO8601(v)) return v;
2108     return (this.format.prefix || '')+d.formatDate(this.format.dateFmt || 'translateDateTime')+(this.format.suffix || '');
2109   }
2110 },
2111
2112 // converts GMT/UTC to local time
2113 format_UTCasLocalTime: function(v) {
2114   if (typeof v=='undefined' || v=='' || v==null)
2115     return '&nbsp;';
2116   else {
2117     var d=new Date();
2118     if (!d.setISO8601(v,-d.getTimezoneOffset())) return v;
2119     return (this.format.prefix || '')+d.formatDate(this.format.dateFmt || 'translateDateTime')+(this.format.suffix || '');
2120   }
2121 },
2122
2123 format_date: function(v) {
2124   if (typeof v=='undefined' || v==null || v=='')
2125     return '&nbsp;';
2126   else {
2127     var d=new Date();
2128     if (!d.setISO8601(v)) return v;
2129     return (this.format.prefix || '')+d.formatDate(this.format.dateFmt || 'translateDate')+(this.format.suffix || '');
2130   }
2131 },
2132
2133 fixHeaders: function(prefix, iconsfirst) {
2134   if (this.sortable) {
2135     switch (this.options.headingSort) {
2136       case 'link':
2137         var a=RicoUtil.wrapChildren(this.hdrCellDiv,'ricoSort',undefined,'a');
2138         a.href = "javascript:void(0)";
2139         a.onclick = this.toggleSort.bindAsEventListener(this);
2140         break;
2141       case 'hover':
2142         this.hdrCellDiv.onclick = this.toggleSort.bindAsEventListener(this);
2143         break;
2144     }
2145   }
2146   this.imgFilter = document.createElement('img');
2147   this.imgFilter.style.display='none';
2148   this.imgFilter.src=Rico.imgDir+this.options.filterImg;
2149   this.imgFilter.className='ricoLG_HdrIcon';
2150   this.imgSort = document.createElement('img');
2151   this.imgSort.style.display='none';
2152   this.imgSort.src=Rico.imgDir+this.options.sortAscendImg;
2153   this.imgSort.className='ricoLG_HdrIcon';
2154   if (iconsfirst) {
2155     this.hdrCellDiv.insertBefore(this.imgSort,this.hdrCellDiv.firstChild);
2156     this.hdrCellDiv.insertBefore(this.imgFilter,this.hdrCellDiv.firstChild);
2157   } else {
2158     this.hdrCellDiv.appendChild(this.imgFilter);
2159     this.hdrCellDiv.appendChild(this.imgSort);
2160   }
2161   if (!this.format.filterUI) {
2162     Event.observe(this.imgFilter, 'click', this.filterClick.bindAsEventListener(this), false);
2163   }
2164 },
2165
2166 filterClick: function(e) {
2167   if (this.filterType==Rico.TableColumn.USERFILTER && this.filterOp=='LIKE') {
2168     this.liveGrid.openKeyword(this.index);
2169   }
2170 },
2171
2172 getValue: function(windowRow) {
2173   return this.liveGrid.buffer.getWindowValue(windowRow,this.index);
2174 },
2175
2176 getBufferCell: function(windowRow) {
2177   return this.liveGrid.buffer.getWindowCell(windowRow,this.index);
2178 },
2179
2180 getBufferAttr: function(windowRow) {
2181   return this.liveGrid.buffer.getWindowAttr(windowRow,this.index);
2182 },
2183
2184 setValue: function(windowRow,newval) {
2185   this.liveGrid.buffer.setWindowValue(windowRow,this.index,newval);
2186 },
2187
2188 _format: function(v) {
2189   return v;
2190 },
2191
2192 _display: function(v,gridCell) {
2193   gridCell.innerHTML=this._format(v);
2194 },
2195
2196 _export: function(v) {
2197   return this._format(v);
2198 },
2199
2200 displayValue: function(windowRow) {
2201   var bufCell=this.getBufferCell(windowRow);
2202   if (bufCell==null) {
2203     this.clearCell(windowRow);
2204     return;
2205   }
2206   var gridCell=this.cell(windowRow);
2207   this._display(bufCell,gridCell,windowRow);
2208   var acceptAttr=this.liveGrid.buffer.options.acceptAttr;
2209   if (acceptAttr.length==0) return;
2210   var bufAttr=this.getBufferAttr(windowRow);
2211   if (bufAttr==null) return;
2212   for (var k=0; k<acceptAttr.length; k++) {
2213     bufAttr=bufAttr['_'+acceptAttr[k]] || '';
2214     switch (acceptAttr[k]) {
2215       case 'style': gridCell.style.cssText=bufAttr; break;
2216       case 'class': gridCell.className=bufAttr; break;
2217       default:      gridCell['_'+acceptAttr[k]]=bufAttr; break;
2218     }
2219   }
2220 }
2221
2222 };
2223
2224 Rico.includeLoaded('ricoLiveGrid.js');