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