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