2 * (c) 2005-2009 Richard Cowin (http://openrico.org)
3 * (c) 2005-2009 Matt Brown (http://dowdybrown.com)
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
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");
21 Rico.Buffer.AjaxXML = Class.create(
22 /** @lends Rico.Buffer.AjaxXML# */
25 * @class Implements buffer for LiveGrid. Loads data from server via a single AJAX call.
26 * @extends Rico.Buffer.Base
29 initialize: function(url,options,ajaxOptions) {
30 Object.extend(this, new Rico.Buffer.Base());
31 Object.extend(this, new Rico.Buffer.AjaxXMLMethods);
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 || {});
41 this.processingRequest=false;
42 this.pendingRequest=-2;
48 Rico.Buffer.AjaxXMLMethods = function() {};
50 Rico.Buffer.AjaxXMLMethods.prototype = {
51 /** @lends Rico.Buffer.AjaxXML# */
52 fetch: function(offset) {
54 this.foundRowCount=true;
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);
65 this.ajaxOptions.onComplete = this.jsUpdate.bind(this,offset);
66 this.dataSource(this.ajaxOptions);
71 this.setTotalRows(this.size);
74 this.liveGrid.refreshContents(offset);
79 * Server did not respond in time... assume that there could have been
80 * an error, and allow requests to be processed again.
82 handleTimedOut: function() {
83 Rico.writeDebugMsg("Request Timed Out");
84 this.liveGrid.showMsg(RicoTranslate.getPhraseById("requestTimedOut"));
87 formQueryHashXML: function(startPos,fetchSize) {
89 id: this.liveGrid.tableId,
90 page_size: (typeof fetchSize=='number') ? fetchSize : this.totalRows,
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;
100 var ePos = anArg.indexOf('=');
101 var argName = anArg.substring( 0, ePos );
102 var argValue = anArg.substring( ePos + 1 );
103 queryHash[argName]=argValue;
110 clearTimer: function() {
111 if(typeof this.timeoutHandler != "number") return;
112 window.clearTimeout(this.timeoutHandler);
113 delete this.timeoutHandler;
116 // used by both XML and SQL buffers
117 jsUpdate: function(startPos, newRows, newAttr, totalRows, errMsg) {
119 this.processingRequest=false;
120 Rico.writeDebugMsg("jsUpdate: "+arguments.length);
122 Rico.writeDebugMsg("jsUpdate: received error="+errMsg);
123 this.liveGrid.showMsg(RicoTranslate.getPhraseById("requestError",errMsg));
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);
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;
147 // used by both XML and SQL buffers
148 ajaxUpdate: function(startPos,request) {
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));
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;
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("-->");
177 this.liveGrid.showMsg('Web server error - client side debugging may be enabled');
180 xmlDoc = RicoUtil.createXmlDocument();
181 xmlDoc.loadXML(request.responseText.substring(nEnd+3));
184 // process children of <ajax-response>
185 var response = xmlDoc.getElementsByTagName("ajax-response");
186 if (response == null || response.length != 1) return false;
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);
200 var rowsElement = ajaxResponse.getElementsByTagName('rows')[0];
202 Rico.writeDebugMsg("ajaxUpdate: invalid response");
203 this.liveGrid.showMsg(RicoTranslate.getPhraseById("invalidResponse"));
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);
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);
225 // specific to XML buffer
226 updateBuffer: function(start, newRows, newAttr) {
227 this.baseRows = newRows;
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);
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));
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);
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);
268 newpos=this.liveGrid.pixeltorow(this.liveGrid.scrollDiv.scrollTop);
269 Rico.writeDebugMsg("updateGrid: newpos="+newpos);
270 this.liveGrid.refreshContents(newpos);
277 Rico.Buffer.AjaxSQL = Class.create(
278 /** @lends Rico.Buffer.AjaxSQL# */
281 * @class Implements buffer for LiveGrid. Loads data from server in chunks as user scrolls through the grid.
282 * @extends Rico.Buffer.AjaxXML
285 initialize: function(url,options,ajaxOptions) {
286 Object.extend(this, new Rico.Buffer.AjaxXML());
287 Object.extend(this, new Rico.Buffer.AjaxSQLMethods());
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 || {});
297 Rico.Buffer.AjaxSQLMethods = function() {};
299 Rico.Buffer.AjaxSQLMethods.prototype = {
300 /** @lends Rico.Buffer.AjaxSQL# */
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();
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;
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();
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;
332 var timeRemaining=Math.ceil((this.sessionEndTime - now) / 60000);
333 this.displaySessionTimer(timeRemaining);
334 this.sessionTimer=setTimeout(this.updateSessionTimer.bind(this),10000);
338 displaySessionTimer: function(msg) {
339 this.timerMsg.innerHTML=' '+msg+' ';
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
346 refresh: function(resetRowCount) {
347 var lastGridPos=this.liveGrid.lastRowPos;
350 this.setTotalRows(0);
351 this.foundRowCount = false;
353 this.liveGrid.clearBookmark();
354 this.liveGrid.clearRows();
355 this.fetch(lastGridPos);
359 * Fetch data from database.
360 * @param offset position (row) within the dataset (-1=clear existing buffer before issuing request)
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;
371 this.setTotalRows(0);
372 this.foundRowCount = false;
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;
388 if (offset >= this.totalRows && this.foundRowCount) return;
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;
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);
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);
405 this.ajaxOptions.onComplete = this.jsUpdate.bind(this,bufferStartPos);
406 this.dataSource(this.ajaxOptions);
410 formQueryHashSQL: function(startPos,fetchSize) {
411 var queryHash=this.formQueryHashXML(startPos,fetchSize);
414 Object.extend(queryHash,this.sortParm);
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;
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);
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;
455 updateBuffer: function(start, newRows, newAttr) {
456 Rico.writeDebugMsg("updateBuffer: start="+start+", # of rows="+this.rcvdRows);
457 if (this.rows.length == 0) { // initial load
460 this.startPos = start;
461 } else if (start > this.startPos) { //appending
462 if (this.startPos + this.rows.length < start) {
465 this.startPos = start;
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);
476 } else { //prepending
477 if (start + newRows.length < this.startPos) {
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);
484 this.startPos = start;
486 this.size = this.rows.length;
489 sortBuffer: function(colnum) {
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();
496 this.sortParm['s'+colnum]=col.getSortDirection();
501 exportAllRows: function(populate,finish) {
502 this.exportPopulate=populate;
503 this.exportFinish=finish;
504 this.sendExportRequest(0);
508 * Send request for print window data
510 sendExportRequest: function(offset) {
511 this.timeoutHandler = setTimeout(this.exportTimedOut.bind(this), this.options.bufferTimeout);
512 this.ajaxOptions.parameters = this.formQueryHashSQL(offset,200);
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);
519 this.ajaxOptions.onComplete = this.jsExportAppend.bind(this,offset);
520 this.dataSource(this.ajaxOptions);
524 exportTimedOut: function() {
525 Rico.writeDebugMsg("Print Request Timed Out");
526 this.liveGrid.showMsg(RicoTranslate.getPhraseById("requestTimedOut"));
530 jsExportAppend: function(startPos, newRows, newAttr, totalRows, errMsg) {
532 Rico.writeDebugMsg("jsExportAppend: "+arguments.length);
534 Rico.writeDebugMsg("jsExportAppend: received error="+errMsg);
535 this.liveGrid.showMsg(RicoTranslate.getPhraseById("requestError",errMsg));
538 this.exportPopulate(newRows,startPos);
539 if (newRows.length==0)
542 this.sendExportRequest(startPos+newRows.length);
545 ajaxExportAppend: function(startPos,request) {
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);
556 this.sendExportRequest(startPos+rows.length);
561 Rico.includeLoaded('ricoLiveGridAjax.js');