Automatically calculate the filesystem path of the application and use
[misc/kostenrechnung] / lib / rico / ricoLiveGridAjax.js
1 /*
2  *  (c) 2005-2009 Richard Cowin (http://openrico.org)
3  *  (c) 2005-2009 Matt Brown (http://dowdybrown.com)
4  *
5  *  Rico is licensed under the Apache License, Version 2.0 (the "License"); you may not use this
6  *  file except in compliance with the License. You may obtain a copy of the License at
7  *
8  *         http://www.apache.org/licenses/LICENSE-2.0
9  *
10  *  Unless required by applicable law or agreed to in writing, software distributed under the
11  *  License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
12  *  either express or implied. See the License for the specific language governing permissions
13  *  and limitations under the License.
14  */
15
16 if(typeof Rico=='undefined') throw("LiveGridAjax requires the Rico JavaScript framework");
17 if(typeof RicoUtil=='undefined') throw("LiveGridAjax requires the RicoUtil object");
18 if(typeof Rico.Buffer=='undefined') throw("LiveGridAjax requires the Rico.Buffer object");
19
20
21 Rico.Buffer.AjaxXML = Class.create(
22 /** @lends Rico.Buffer.AjaxXML# */
23 {
24 /**
25  * @class Implements buffer for LiveGrid. Loads data from server via a single AJAX call.
26  * @extends Rico.Buffer.Base
27  * @constructs
28  */
29   initialize: function(url,options,ajaxOptions) {
30     Object.extend(this, new Rico.Buffer.Base());
31     Object.extend(this, new Rico.Buffer.AjaxXMLMethods);
32     this.dataSource=url;
33     this.options.bufferTimeout=20000;            // time to wait for ajax response (milliseconds)
34     this.options.requestParameters=[];
35     this.options.waitMsg=RicoTranslate.getPhraseById("waitForData");  // replace this with an image tag if you prefer
36     this.options.canFilter=true;
37     Object.extend(this.options, options || {});
38     this.ajaxOptions = { parameters: null, method : 'get' };
39     Object.extend(this.ajaxOptions, ajaxOptions || {});
40     this.requestCount=0;
41     this.processingRequest=false;
42     this.pendingRequest=-2;
43     this.fetchData=true;
44     this.sortParm={};
45   }
46 });
47
48 Rico.Buffer.AjaxXMLMethods = function() {};
49
50 Rico.Buffer.AjaxXMLMethods.prototype = {
51 /** @lends Rico.Buffer.AjaxXML# */
52   fetch: function(offset) {
53     if (this.fetchData) {
54       this.foundRowCount=true;
55       this.fetchData=false;
56       this.processingRequest=true;
57       this.liveGrid.showMsg(this.options.waitMsg);
58       this.timeoutHandler = setTimeout( this.handleTimedOut.bind(this), this.options.bufferTimeout);
59       this.ajaxOptions.parameters = this.formQueryHashXML(0,-1);
60       Rico.writeDebugMsg('sending request');
61       if (typeof this.dataSource=='string') {
62         this.ajaxOptions.onComplete = this.ajaxUpdate.bind(this,offset);
63         new Ajax.Request(this.dataSource, this.ajaxOptions);
64       } else {
65         this.ajaxOptions.onComplete = this.jsUpdate.bind(this,offset);
66         this.dataSource(this.ajaxOptions);
67       }
68     } else {
69       if (offset < 0) {
70         this.applyFilters();
71         this.setTotalRows(this.size);
72         offset=0;
73       }
74       this.liveGrid.refreshContents(offset);
75     }
76   },
77
78 /**
79  * Server did not respond in time... assume that there could have been
80  * an error, and allow requests to be processed again.
81  */
82   handleTimedOut: function() {
83     Rico.writeDebugMsg("Request Timed Out");
84     this.liveGrid.showMsg(RicoTranslate.getPhraseById("requestTimedOut"));
85   },
86
87   formQueryHashXML: function(startPos,fetchSize) {
88     var queryHash= {
89       id: this.liveGrid.tableId,
90       page_size: (typeof fetchSize=='number') ? fetchSize : this.totalRows,
91       offset: startPos
92     };
93     if (!this.foundRowCount) queryHash['get_total']='true';
94     if (this.options.requestParameters) {
95       for ( var i=0; i < this.options.requestParameters.length; i++ ) {
96         var anArg = this.options.requestParameters[i];
97         if ( anArg.name != undefined && anArg.value != undefined ) {
98           queryHash[anArg.name]=anArg.value;
99         } else {
100           var ePos  = anArg.indexOf('=');
101           var argName  = anArg.substring( 0, ePos );
102           var argValue = anArg.substring( ePos + 1 );
103           queryHash[argName]=argValue;
104         }
105       }
106     }
107     return queryHash;
108   },
109
110   clearTimer: function() {
111     if(typeof this.timeoutHandler != "number") return;
112     window.clearTimeout(this.timeoutHandler);
113     delete this.timeoutHandler;
114   },
115
116   // used by both XML and SQL buffers
117   jsUpdate: function(startPos, newRows, newAttr, totalRows, errMsg) {
118     this.clearTimer();
119     this.processingRequest=false;
120     Rico.writeDebugMsg("jsUpdate: "+arguments.length);
121     if (errMsg) {
122       Rico.writeDebugMsg("jsUpdate: received error="+errMsg);
123       this.liveGrid.showMsg(RicoTranslate.getPhraseById("requestError",errMsg));
124       return;
125     }
126     this.rcvdRows = newRows.length;
127     if (typeof totalRows=='number') {
128       this.rowcntContent = totalRows.toString();
129       this.rcvdRowCount = true;
130       this.foundRowCount = true;
131       Rico.writeDebugMsg("jsUpdate: found RowCount="+this.rowcntContent);
132     }
133     this.updateBuffer(startPos, newRows, newAttr);
134     if (this.options.onAjaxUpdate)
135       this.options.onAjaxUpdate();
136     this.updateGrid(startPos);
137     if (this.options.TimeOut && this.timerMsg)
138       this.restartSessionTimer();
139     if (this.pendingRequest>=-1) {
140       var offset=this.pendingRequest;
141       Rico.writeDebugMsg("jsUpdate: found pending request for offset="+offset);
142       this.pendingRequest=-2;
143       this.fetch(offset);
144     }
145   },
146
147   // used by both XML and SQL buffers
148   ajaxUpdate: function(startPos,request) {
149     this.clearTimer();
150     this.processingRequest=false;
151     if (request.status != 200) {
152       Rico.writeDebugMsg("ajaxUpdate: received http error="+request.status);
153       this.liveGrid.showMsg(RicoTranslate.getPhraseById("httpError",request.status));
154       return;
155     }
156     if (!this.processResponse(startPos,request)) return;
157     if (this.options.onAjaxUpdate)
158       this.options.onAjaxUpdate();
159     this.updateGrid(startPos);
160     if (this.options.TimeOut && this.timerMsg)
161       this.restartSessionTimer();
162     if (this.pendingRequest>=-1) {
163       var offset=this.pendingRequest;
164       Rico.writeDebugMsg("ajaxUpdate: found pending request for offset="+offset);
165       this.pendingRequest=-2;
166       this.fetch(offset);
167     }
168   },
169   
170   // used by both XML and SQL buffers
171   processResponse: function(startPos,request) {
172     // The response text may contain META DATA for debugging if client side debugging is enabled in VS
173     var xmlDoc = request.responseXML;
174     if (request.responseText.substring(0, 4) == "<!--") {
175       var nEnd = request.responseText.indexOf("-->");
176       if (nEnd == -1) {
177         this.liveGrid.showMsg('Web server error - client side debugging may be enabled');
178         return false;
179       }
180       xmlDoc = RicoUtil.createXmlDocument();
181       xmlDoc.loadXML(request.responseText.substring(nEnd+3));
182     }
183
184     // process children of <ajax-response>
185     var response = xmlDoc.getElementsByTagName("ajax-response");
186     if (response == null || response.length != 1) return false;
187     this.rcvdRows = 0;
188     this.rcvdRowCount = false;
189     var ajaxResponse=response[0];
190     var debugtags = ajaxResponse.getElementsByTagName('debug');
191     for (var i=0; i<debugtags.length; i++)
192       Rico.writeDebugMsg("ajaxUpdate: debug msg "+i+": "+RicoUtil.getContentAsString(debugtags[i],this.options.isEncoded));
193     var error = ajaxResponse.getElementsByTagName('error');
194     if (error.length > 0) {
195       var msg=RicoUtil.getContentAsString(error[0],this.options.isEncoded);
196       alert("Data provider returned an error:\n"+msg);
197       Rico.writeDebugMsg("Data provider returned an error:\n"+msg);
198       return false;
199     }
200     var rowsElement = ajaxResponse.getElementsByTagName('rows')[0];
201     if (!rowsElement) {
202       Rico.writeDebugMsg("ajaxUpdate: invalid response");
203       this.liveGrid.showMsg(RicoTranslate.getPhraseById("invalidResponse"));
204       return false;
205     }
206     var rowcnttags = ajaxResponse.getElementsByTagName('rowcount');
207     if (rowcnttags && rowcnttags.length==1) {
208       this.rowcntContent = RicoUtil.getContentAsString(rowcnttags[0],this.options.isEncoded);
209       this.rcvdRowCount = true;
210       this.foundRowCount = true;
211       Rico.writeDebugMsg("ajaxUpdate: found RowCount="+this.rowcntContent);
212     }
213
214     // process <rows>
215     this.updateUI = rowsElement.getAttribute("update_ui") == "true";
216     this.rcvdOffset = rowsElement.getAttribute("offset");
217     Rico.writeDebugMsg("ajaxUpdate: rcvdOffset="+this.rcvdOffset);
218     var newRows = this.dom2jstable(rowsElement);
219     var newAttr = (this.options.acceptAttr.length > 0) ? this.dom2jstableAttr(rowsElement) : false;
220     this.rcvdRows = newRows.length;
221     this.updateBuffer(startPos, newRows, newAttr);
222     return true;
223   },
224
225   // specific to XML buffer
226   updateBuffer: function(start, newRows, newAttr) {
227     this.baseRows = newRows;
228     this.attr = newAttr;
229     Rico.writeDebugMsg("updateBuffer: # of rows="+this.rcvdRows);
230     this.rcvdRowCount=true;
231     this.rowcntContent=this.rcvdRows;
232     if (typeof this.delayedSortCol=='number')
233       this.sortBuffer(this.delayedSortCol);
234     this.applyFilters();
235     this.startPos = 0;
236   },
237
238   // used by both XML and SQL buffers
239   updateGrid: function(offset) {
240     Rico.writeDebugMsg("updateGrid, size="+this.size+' rcv cnt type='+typeof(this.rowcntContent));
241     var newpos;
242     if (this.rcvdRowCount==true) {
243       Rico.writeDebugMsg("found row cnt: "+this.rowcntContent);
244       var eofrow=parseInt(this.rowcntContent,10);
245       var lastTotalRows=this.totalRows;
246       if (!isNaN(eofrow) && eofrow!=lastTotalRows) {
247         this.setTotalRows(eofrow);
248         newpos=Math.min(this.liveGrid.topOfLastPage(),offset);
249         Rico.writeDebugMsg("updateGrid: new rowcnt="+eofrow+" newpos="+newpos);
250         if (lastTotalRows==0 && this.liveGrid.sizeTo=='data')
251           this.liveGrid.adjustPageSize();
252         this.liveGrid.scrollToRow(newpos);
253         if ( this.isInRange(newpos) ) {
254           this.liveGrid.refreshContents(newpos);
255         } else {
256           this.fetch(newpos);
257         }
258         return;
259       }
260     } else {
261       var lastbufrow=offset+this.rcvdRows;
262       if (lastbufrow>this.totalRows) {
263         var newcnt=lastbufrow;
264         Rico.writeDebugMsg("extending totrows to "+newcnt);
265         this.setTotalRows(newcnt);
266       }
267     }
268     newpos=this.liveGrid.pixeltorow(this.liveGrid.scrollDiv.scrollTop);
269     Rico.writeDebugMsg("updateGrid: newpos="+newpos);
270     this.liveGrid.refreshContents(newpos);
271   }
272
273 };
274
275
276
277 Rico.Buffer.AjaxSQL = Class.create(
278 /** @lends Rico.Buffer.AjaxSQL# */
279 {
280 /**
281  * @class Implements buffer for LiveGrid. Loads data from server in chunks as user scrolls through the grid.
282  * @extends Rico.Buffer.AjaxXML
283  * @constructs
284  */
285   initialize: function(url,options,ajaxOptions) {
286     Object.extend(this, new Rico.Buffer.AjaxXML());
287     Object.extend(this, new Rico.Buffer.AjaxSQLMethods());
288     this.dataSource=url;
289     this.options.canFilter=true;
290     this.options.largeBufferSize  = 7.0;   // 7 pages
291     this.options.nearLimitFactor  = 1.0;   // 1 page
292     Object.extend(this.options, options || {});
293     Object.extend(this.ajaxOptions, ajaxOptions || {});
294   }
295 });
296
297 Rico.Buffer.AjaxSQLMethods = function() {};
298
299 Rico.Buffer.AjaxSQLMethods.prototype = {
300 /** @lends Rico.Buffer.AjaxSQL# */
301
302   registerGrid: function(liveGrid) {
303     this.liveGrid = liveGrid;
304     this.sessionExpired=false;
305     this.timerMsg=$(liveGrid.tableId+'_timer');
306     if (this.options.TimeOut && this.timerMsg) {
307       if (!this.timerMsg.title) this.timerMsg.title=RicoTranslate.getPhraseById("sessionExpireMinutes");
308       this.restartSessionTimer();
309     }
310   },
311
312   setBufferSize: function(pageSize) {
313     this.maxFetchSize = Math.max(50,parseInt(this.options.largeBufferSize * pageSize,10));
314     this.nearLimit = parseInt(this.options.nearLimitFactor * pageSize,10);
315     this.maxBufferSize = this.maxFetchSize * 3;
316   },
317
318   restartSessionTimer: function() {
319     if (this.sessionExpired==true) return;
320     this.sessionEndTime = (new Date()).getTime() + this.options.TimeOut*60000;
321     if (this.sessionTimer) clearTimeout(this.sessionTimer);
322     this.updateSessionTimer();
323   },
324
325   updateSessionTimer: function() {
326     var now=(new Date()).getTime();
327     if (now > this.sessionEndTime) {
328       this.displaySessionTimer(RicoTranslate.getPhraseById("sessionExpired"));
329       this.timerMsg.style.backgroundColor="red";
330       this.sessionExpired=true;
331     } else {
332       var timeRemaining=Math.ceil((this.sessionEndTime - now) / 60000);
333       this.displaySessionTimer(timeRemaining);
334       this.sessionTimer=setTimeout(this.updateSessionTimer.bind(this),10000);
335     }
336   },
337
338   displaySessionTimer: function(msg) {
339     this.timerMsg.innerHTML='&nbsp;'+msg+'&nbsp;';
340   },
341
342   /**
343    * Update the grid with fresh data from the database, maintaining scroll position.
344    * @param resetRowCount indicates whether the total row count should be refreshed as well
345    */
346   refresh: function(resetRowCount) {
347     var lastGridPos=this.liveGrid.lastRowPos;
348     this.clear();
349     if (resetRowCount) {
350       this.setTotalRows(0);
351       this.foundRowCount = false;
352     }
353     this.liveGrid.clearBookmark();
354     this.liveGrid.clearRows();
355     this.fetch(lastGridPos);
356   },
357
358   /**
359    * Fetch data from database.
360    * @param offset position (row) within the dataset (-1=clear existing buffer before issuing request)
361    */
362   fetch: function(offset) {
363     Rico.writeDebugMsg("AjaxSQL fetch: offset="+offset+', lastOffset='+this.lastOffset);
364     if (this.processingRequest) {
365       Rico.writeDebugMsg("AjaxSQL fetch: queue request");
366       this.pendingRequest=offset;
367       return;
368     }
369     if (offset < 0) {
370       this.clear();
371       this.setTotalRows(0);
372       this.foundRowCount = false;
373       offset=0;
374     }
375     var lastOffset = this.lastOffset;
376     this.lastOffset = offset;
377     if (this.isInRange(offset)) {
378       Rico.writeDebugMsg("AjaxSQL fetch: in buffer");
379       this.liveGrid.refreshContents(offset);
380       if (offset > lastOffset) {
381         if (offset+this.liveGrid.pageSize < this.endPos()-this.nearLimit) return;
382         if (this.endPos()==this.totalRows && this.foundRowCount) return;
383       } else if (offset < lastOffset) {
384         if (offset > this.startPos+this.nearLimit) return;
385         if (this.startPos==0) return;
386       } else return;
387     }
388     if (offset >= this.totalRows && this.foundRowCount) return;
389
390     this.processingRequest=true;
391     Rico.writeDebugMsg("AjaxSQL fetch: processing offset="+offset);
392     var bufferStartPos = this.getFetchOffset(offset);
393     var fetchSize = this.getFetchSize(bufferStartPos);
394     var partialLoaded = false;
395
396     this.liveGrid.showMsg(this.options.waitMsg);
397     this.timeoutHandler = setTimeout( this.handleTimedOut.bind(this), this.options.bufferTimeout);
398     this.ajaxOptions.parameters = this.formQueryHashSQL(bufferStartPos,fetchSize);
399     this.requestCount++;
400     Rico.writeDebugMsg('sending req #'+this.requestCount);
401     if (typeof this.dataSource=='string') {
402       this.ajaxOptions.onComplete = this.ajaxUpdate.bind(this,bufferStartPos);
403       new Ajax.Request(this.dataSource, this.ajaxOptions);
404     } else {
405       this.ajaxOptions.onComplete = this.jsUpdate.bind(this,bufferStartPos);
406       this.dataSource(this.ajaxOptions);
407     }
408   },
409
410   formQueryHashSQL: function(startPos,fetchSize) {
411     var queryHash=this.formQueryHashXML(startPos,fetchSize);
412
413     // sort
414     Object.extend(queryHash,this.sortParm);
415
416     // filters
417     for (var n=0; n<this.liveGrid.columns.length; n++) {
418       var c=this.liveGrid.columns[n];
419       if (c.filterType == Rico.TableColumn.UNFILTERED) continue;
420       var colnum=typeof(c.format.filterCol)=='number' ? c.format.filterCol : c.index;
421       queryHash['f['+colnum+'][op]']=c.filterOp;
422       queryHash['f['+colnum+'][len]']=c.filterValues.length;
423       for (var i=0; i<c.filterValues.length; i++) {
424         var fval=c.filterValues[i];
425         if (c.filterOp=='LIKE' && fval.indexOf('*')==-1) fval='*'+fval+'*';
426         queryHash['f['+colnum+']['+i+']']=fval;
427       }
428     }
429     return queryHash;
430   },
431
432   getFetchSize: function(adjustedOffset) {
433     var adjustedSize = 0;
434     if (adjustedOffset >= this.startPos) { //appending
435       var endFetchOffset = this.maxFetchSize + adjustedOffset;
436       adjustedSize = endFetchOffset - adjustedOffset;
437       if(adjustedOffset == 0 && adjustedSize < this.maxFetchSize)
438         adjustedSize = this.maxFetchSize;
439       Rico.writeDebugMsg("getFetchSize/append, adjustedSize="+adjustedSize+" adjustedOffset="+adjustedOffset+' endFetchOffset='+endFetchOffset);
440     } else { //prepending
441       adjustedSize = Math.min(this.startPos - adjustedOffset,this.maxFetchSize);
442     }
443     return adjustedSize;
444   },
445
446   getFetchOffset: function(offset) {
447     var adjustedOffset = offset;
448     if (offset > this.startPos)
449       adjustedOffset = Math.max(offset, this.endPos());  //appending
450     else if (offset + this.maxFetchSize >= this.startPos)
451       adjustedOffset = Math.max(this.startPos - this.maxFetchSize, 0);  //prepending
452     return adjustedOffset;
453   },
454
455   updateBuffer: function(start, newRows, newAttr) {
456     Rico.writeDebugMsg("updateBuffer: start="+start+", # of rows="+this.rcvdRows);
457     if (this.rows.length == 0) { // initial load
458       this.rows = newRows;
459       this.attr = newAttr;
460       this.startPos = start;
461     } else if (start > this.startPos) { //appending
462       if (this.startPos + this.rows.length < start) {
463         this.rows =  newRows;
464         this.attr = newAttr;
465         this.startPos = start;
466       } else {
467         this.rows = this.rows.concat( newRows.slice(0, newRows.length));
468         if (this.attr) this.attr = this.attr.concat( newAttr.slice(0, newAttr.length));
469         if (this.rows.length > this.maxBufferSize) {
470           var fullSize = this.rows.length;
471           this.rows = this.rows.slice(this.rows.length - this.maxBufferSize, this.rows.length);
472           if (this.attr) this.attr = this.attr.slice(this.attr.length - this.maxBufferSize, this.attr.length);
473           this.startPos = this.startPos +  (fullSize - this.rows.length);
474         }
475       }
476     } else { //prepending
477       if (start + newRows.length < this.startPos) {
478         this.rows =  newRows;
479       } else {
480         this.rows = newRows.slice(0, this.startPos).concat(this.rows);
481         if (this.maxBufferSize && this.rows.length > this.maxBufferSize)
482           this.rows = this.rows.slice(0, this.maxBufferSize);
483       }
484       this.startPos =  start;
485     }
486     this.size = this.rows.length;
487   },
488
489   sortBuffer: function(colnum) {
490     this.sortParm={};
491     var col=this.liveGrid.columns[colnum];
492     if (this.options.sortParmFmt) {
493       this.sortParm['sort_col']=col[this.options.sortParmFmt];
494       this.sortParm['sort_dir']=col.getSortDirection();
495     } else {
496       this.sortParm['s'+colnum]=col.getSortDirection();
497     }
498     this.clear();
499   },
500
501   exportAllRows: function(populate,finish) {
502     this.exportPopulate=populate;
503     this.exportFinish=finish;
504     this.sendExportRequest(0);
505   },
506
507 /**
508  * Send request for print window data
509  */
510   sendExportRequest: function(offset) {
511     this.timeoutHandler = setTimeout(this.exportTimedOut.bind(this), this.options.bufferTimeout);
512     this.ajaxOptions.parameters = this.formQueryHashSQL(offset,200);
513     this.requestCount++;
514     Rico.writeDebugMsg('sending export req #'+this.requestCount);
515     if (typeof this.dataSource=='string') {
516       this.ajaxOptions.onComplete = this.ajaxExportAppend.bind(this,offset);
517       new Ajax.Request(this.dataSource, this.ajaxOptions);
518     } else {
519       this.ajaxOptions.onComplete = this.jsExportAppend.bind(this,offset);
520       this.dataSource(this.ajaxOptions);
521     }
522   },
523
524   exportTimedOut: function() {
525     Rico.writeDebugMsg("Print Request Timed Out");
526     this.liveGrid.showMsg(RicoTranslate.getPhraseById("requestTimedOut"));
527     this.exportFinish();
528   },
529
530   jsExportAppend: function(startPos, newRows, newAttr, totalRows, errMsg) {
531     this.clearTimer();
532     Rico.writeDebugMsg("jsExportAppend: "+arguments.length);
533     if (errMsg) {
534       Rico.writeDebugMsg("jsExportAppend: received error="+errMsg);
535       this.liveGrid.showMsg(RicoTranslate.getPhraseById("requestError",errMsg));
536       return;
537     }
538     this.exportPopulate(newRows,startPos);
539     if (newRows.length==0)
540       this.exportFinish();
541     else
542       this.sendExportRequest(startPos+newRows.length);
543   },
544
545   ajaxExportAppend: function(startPos,request) {
546     this.clearTimer();
547     Rico.writeDebugMsg("ajaxExportAppend");
548     var response = request.responseXML.getElementsByTagName("ajax-response");
549     if (response == null || response.length != 1) return;
550     var rowsElement = response[0].getElementsByTagName('rows')[0];
551     var rows=this.dom2jstable(rowsElement);
552     this.exportPopulate(rows,startPos);
553     if (rows.length==0)
554       this.exportFinish();
555     else
556       this.sendExportRequest(startPos+rows.length);
557   }
558
559 };
560
561 Rico.includeLoaded('ricoLiveGridAjax.js');