2 * (c) 2005-2011 Richard Cowin (http://openrico.org)
3 * (c) 2005-2011 Matt Brown (http://dowdybrown.com)
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
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");
18 if (!Rico.Buffer) Rico.Buffer = {};
20 Rico.Buffer.AjaxLoadOnce = function(url,options,ajaxOptions) {
21 this.initialize(url,options,ajaxOptions);
24 Rico.Buffer.AjaxLoadOnce.prototype = {
26 * @class Implements buffer for LiveGrid. Loads data from server via a single AJAX call.
27 * @extends Rico.Buffer.Base
30 initialize: function(url,options,ajaxOptions) {
31 Rico.extend(this, new Rico.Buffer.Base());
32 Rico.extend(this, Rico.Buffer.AjaxXMLMethods);
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 || {});
43 this.processingRequest=false;
44 this.pendingRequest=-2;
50 Rico.Buffer.AjaxXMLMethods = {
52 /** @lends Rico.Buffer.AjaxLoadOnce# */
53 fetch: function(offset) {
55 this.foundRowCount=true;
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');
63 if (typeof this.dataSource=='string') {
64 this.ajaxOptions.onComplete = function(xhr) { self.ajaxUpdate(offset,xhr); };
65 new Rico.ajaxRequest(this.dataSource, this.ajaxOptions);
67 this.ajaxOptions.onComplete = function(newRows, newStyle, totalRows, errMsg) { self.jsUpdate(offset, newRows, newStyle, totalRows, errMsg); };
68 this.dataSource(this.ajaxOptions);
73 this.setTotalRows(this.size);
76 this.liveGrid.refreshContents(offset);
81 * Server did not respond in time... assume that there could have been
82 * an error, and allow requests to be processed again.
84 handleTimedOut: function() {
85 Rico.log("Request Timed Out");
86 this.liveGrid.showMsg(Rico.getPhraseById("requestTimedOut"));
89 formQueryHashXML: function(startPos,fetchSize) {
91 id: this.liveGrid.tableId,
92 page_size: (typeof fetchSize=='number') ? fetchSize : this.totalRows,
93 offset: startPos.toString()
95 queryHash[this.liveGrid.actionId]="query";
96 if (this.options.requestParameters) {
97 for ( var i=0; i < this.options.requestParameters.length; i++ ) {
98 var anArg = this.options.requestParameters[i];
99 if ( anArg.name != undefined && anArg.value != undefined ) {
100 queryHash[anArg.name]=anArg.value;
102 var ePos = anArg.indexOf('=');
103 var argName = anArg.substring( 0, ePos );
104 var argValue = anArg.substring( ePos + 1 );
105 queryHash[argName]=argValue;
112 clearTimer: function() {
113 if(typeof this.timeoutHandler != "number") return;
114 window.clearTimeout(this.timeoutHandler);
115 delete this.timeoutHandler;
118 // used by both LoadOnce and SQL buffers
119 jsUpdate: function(startPos, newRows, newStyle, totalRows, errMsg) {
121 this.processingRequest=false;
122 Rico.log("jsUpdate: "+arguments.length);
124 Rico.log("jsUpdate: received error="+errMsg);
125 this.liveGrid.showMsg(Rico.getPhraseById("requestError",errMsg));
128 this.rcvdRows = newRows.length;
129 if (typeof totalRows=='number') {
130 this.rowcntContent = totalRows.toString();
131 this.rcvdRowCount = true;
132 this.foundRowCount = true;
133 Rico.log("jsUpdate: found RowCount="+this.rowcntContent);
135 this.updateBuffer(startPos, newRows, newStyle);
136 if (this.options.onAjaxUpdate)
137 this.options.onAjaxUpdate();
138 this.updateGrid(startPos);
139 if (this.options.TimeOut && this.timerMsg)
140 this.restartSessionTimer();
141 if (this.pendingRequest>=-1) {
142 var offset=this.pendingRequest;
143 Rico.log("jsUpdate: found pending request for offset="+offset);
144 this.pendingRequest=-2;
149 // used by both LoadOnce and SQL buffers
150 ajaxUpdate: function(startPos,xhr) {
152 this.processingRequest=false;
153 if (xhr.status != 200) {
154 Rico.log("ajaxUpdate: received http error="+xhr.status);
155 this.liveGrid.showMsg(Rico.getPhraseById("httpError",xhr.status));
158 Rico.log("ajaxUpdate: startPos="+startPos);
159 this._responseHandler=this['processResponse'+this.options.fmt.toUpperCase()];
160 if (!this._responseHandler(startPos,xhr)) return;
161 if (this.options.onAjaxUpdate)
162 this.options.onAjaxUpdate();
163 this.updateGrid(startPos);
164 if (this.options.TimeOut && this.timerMsg)
165 this.restartSessionTimer();
166 if (this.pendingRequest>=-1) {
167 var offset=this.pendingRequest;
168 Rico.log("ajaxUpdate: found pending request for offset="+offset);
169 this.pendingRequest=-2;
174 // used by both LoadOnce and SQL buffers
175 processResponseXML: function(startPos,request) {
176 // The response text may contain META DATA for debugging if client side debugging is enabled in VS
\r
177 var xmlDoc = request.responseXML;
\r
178 if (request.responseText.substring(0, 4) == "<!--") {
\r
179 var nEnd = request.responseText.indexOf("-->");
\r
181 this.liveGrid.showMsg('Web server error - client side debugging may be enabled');
\r
184 xmlDoc = Rico.createXmlDocument();
\r
185 xmlDoc.loadXML(request.responseText.substring(nEnd+3));
\r
189 alert("Data provider returned an invalid XML response");
190 Rico.log("Data provider returned an invalid XML response");
194 // process children of <ajax-response>
195 var response = xmlDoc.getElementsByTagName("ajax-response");
196 if (response == null || response.length != 1) {
197 alert("Received invalid response from server");
200 Rico.log("Processing ajax-response");
202 this.rcvdRowCount = false;
203 var ajaxResponse=response[0];
204 var debugtags = ajaxResponse.getElementsByTagName('debug');
205 for (var i=0; i<debugtags.length; i++)
206 Rico.log("ajaxUpdate: debug msg "+i+": "+Rico.getContentAsString(debugtags[i],this.options.isEncoded));
207 var error = ajaxResponse.getElementsByTagName('error');
208 if (error.length > 0) {
209 var msg=Rico.getContentAsString(error[0],this.options.isEncoded);
210 alert("Data provider returned an error:\n"+msg);
211 Rico.log("Data provider returned an error:\n"+msg);
214 var rowsElement = ajaxResponse.getElementsByTagName('rows')[0];
216 Rico.log("ajaxUpdate: invalid response");
217 this.liveGrid.showMsg(Rico.getPhraseById("invalidResponse"));
220 var rowcnttags = ajaxResponse.getElementsByTagName('rowcount');
221 if (rowcnttags && rowcnttags.length==1) {
222 this.rowcntContent = Rico.getContentAsString(rowcnttags[0],this.options.isEncoded);
223 this.rcvdRowCount = true;
224 this.foundRowCount = true;
225 Rico.log("ajaxUpdate: found RowCount="+this.rowcntContent);
229 this.updateUI = rowsElement.getAttribute("update_ui") == "true";
230 this.rcvdOffset = rowsElement.getAttribute("offset");
231 Rico.log("ajaxUpdate: rcvdOffset="+this.rcvdOffset);
232 var newRows = this.dom2jstable(rowsElement);
233 var newStyle = (this.options.acceptStyle) ? this.dom2jstableStyle(rowsElement) : false;
234 this.rcvdRows = newRows.length;
235 this.updateBuffer(startPos, newRows, newStyle);
239 dom2jstableStyle: function(rowsElement,firstRow) {
240 Rico.log("dom2jstableStyle start");
242 var trs = rowsElement.getElementsByTagName("tr");
243 for ( var i=firstRow || 0; i < trs.length; i++ ) {
245 var cells = trs[i].getElementsByTagName("td");
246 for ( var j=0; j < cells.length ; j++ ) {
247 row[j]=cells[j].getAttribute('style') || '';
251 Rico.log("dom2jstableStyle end");
255 processResponseJSON: function(startPos,request) {
256 var json = Rico.getJSON(request);
257 if (!json || json == null) {
258 alert("Data provider returned an invalid JSON response");
259 Rico.log("Data provider returned an invalid JSON response");
264 for (var i=0; i<json.debug.length; i++)
265 Rico.writeDebugMsg("debug msg "+i+": "+json.debug[i]);
268 alert("Data provider returned an error:\n"+json.error);
269 Rico.writeDebugMsg("Data provider returned an error:\n"+json.error);
274 this.rowcntContent = json.rowcount;
275 this.rcvdRowCount = true;
276 this.foundRowCount = true;
277 Rico.writeDebugMsg("loadRows, found RowCount="+json.rowcount);
280 this.rcvdRows = json.rows.length;
281 this.updateBuffer(startPos, json.rows, json.styles);
285 // specific to LoadOnce buffer
286 updateBuffer: function(start, newRows, newStyle) {
287 this.baseRows = newRows;
288 this.attr = newStyle;
289 Rico.log("updateBuffer: # of rows="+this.rcvdRows);
290 this.rcvdRowCount=true;
291 this.rowcntContent=this.rcvdRows;
292 if (typeof this.delayedSortCol=='number')
293 this.sortBuffer(this.delayedSortCol);
298 // used by both LoadOnce and SQL buffers
299 updateGrid: function(offset) {
300 Rico.log("updateGrid, size="+this.size+' rcv cnt type='+typeof(this.rowcntContent));
302 if (this.rcvdRowCount==true) {
303 Rico.log("found row cnt: "+this.rowcntContent);
304 var eofrow=parseInt(this.rowcntContent,10);
305 var lastTotalRows=this.totalRows;
306 if (!isNaN(eofrow) && eofrow!=lastTotalRows) {
307 this.setTotalRows(eofrow);
308 newpos=Math.min(this.liveGrid.topOfLastPage(),offset);
309 Rico.log("updateGrid: new rowcnt="+eofrow+" newpos="+newpos);
310 this.liveGrid.scrollToRow(newpos);
311 if ( this.isInRange(newpos) ) {
312 this.liveGrid.refreshContents(newpos);
319 var lastbufrow=offset+this.rcvdRows;
320 if (lastbufrow>this.totalRows) {
321 var newcnt=lastbufrow;
322 Rico.log("extending totrows to "+newcnt);
323 this.setTotalRows(newcnt);
326 newpos=this.liveGrid.pixeltorow(this.liveGrid.scrollDiv.scrollTop);
327 Rico.log("updateGrid: newpos="+newpos);
328 this.liveGrid.refreshContents(newpos);
335 Rico.Buffer.AjaxSQL = function(url,options,ajaxOptions) {
336 this.initialize(url,options,ajaxOptions);
339 Rico.Buffer.AjaxSQL.prototype = {
341 * @class Implements buffer for LiveGrid. Loads data from server in chunks as user scrolls through the grid.
342 * @extends Rico.Buffer.AjaxLoadOnce
345 initialize: function(url,options,ajaxOptions) {
346 Rico.extend(this, new Rico.Buffer.AjaxLoadOnce());
347 Rico.extend(this, Rico.Buffer.AjaxSQLMethods);
349 this.options.canFilter=true;
350 this.options.largeBufferSize = 7.0; // 7 pages
351 this.options.nearLimitFactor = 1.0; // 1 page
352 this.options.canRefresh=true;
353 Rico.extend(this.options, options || {});
354 Rico.extend(this.ajaxOptions, ajaxOptions || {});
358 Rico.Buffer.AjaxSQLMethods = {
359 /** @lends Rico.Buffer.AjaxSQL# */
361 registerGrid: function(liveGrid) {
362 this.liveGrid = liveGrid;
363 this.sessionExpired=false;
364 this.timerMsg=document.getElementById(liveGrid.tableId+'_timer');
365 if (this.options.TimeOut && this.timerMsg) {
366 if (!this.timerMsg.title) this.timerMsg.title=Rico.getPhraseById("sessionExpireMinutes");
367 this.restartSessionTimer();
371 setBufferSize: function(pageSize) {
372 this.maxFetchSize = Math.max(50,parseInt(this.options.largeBufferSize * pageSize,10));
373 this.nearLimit = parseInt(this.options.nearLimitFactor * pageSize,10);
374 this.maxBufferSize = this.maxFetchSize * 3;
377 restartSessionTimer: function() {
378 if (this.sessionExpired==true) return;
379 this.sessionEndTime = (new Date()).getTime() + this.options.TimeOut*60000;
380 if (this.sessionTimer) clearTimeout(this.sessionTimer);
381 this.updateSessionTimer();
384 updateSessionTimer: function() {
385 var now=(new Date()).getTime();
386 if (now > this.sessionEndTime) {
387 this.displaySessionTimer(Rico.getPhraseById("sessionExpired"));
388 this.timerMsg.style.backgroundColor="red";
389 this.sessionExpired=true;
391 var timeRemaining=Math.ceil((this.sessionEndTime - now) / 60000);
392 this.displaySessionTimer(timeRemaining);
393 this.sessionTimer=Rico.runLater(10000,this,'updateSessionTimer');
397 displaySessionTimer: function(msg) {
398 this.timerMsg.innerHTML=' '+msg+' ';
402 * Update the grid with fresh data from the database, maintaining scroll position.
403 * @param resetRowCount indicates whether the total row count should be refreshed as well
405 refresh: function(resetRowCount) {
406 var lastGridPos=this.liveGrid.lastRowPos;
\r
409 this.setTotalRows(0);
410 this.foundRowCount = false;
412 this.liveGrid.clearBookmark();
413 this.liveGrid.clearRows();
414 this.fetch(lastGridPos);
418 * Fetch data from database.
419 * @param offset position (row) within the dataset (-1=clear existing buffer before issuing request)
421 fetch: function(offset) {
422 Rico.log("AjaxSQL fetch: offset="+offset+', lastOffset='+this.lastOffset);
423 if (this.processingRequest) {
424 Rico.log("AjaxSQL fetch: queue request");
425 this.pendingRequest=offset;
428 if ((typeof offset == 'undefined') || (offset < 0)) {
430 this.setTotalRows(0);
431 this.foundRowCount = false;
434 var lastOffset = this.lastOffset;
435 this.lastOffset = offset;
436 if (this.isInRange(offset)) {
437 Rico.log("AjaxSQL fetch: in buffer");
438 this.liveGrid.refreshContents(offset);
439 if (offset > lastOffset) {
440 if (offset+this.liveGrid.pageSize < this.endPos()-this.nearLimit) return;
441 if (this.endPos()==this.totalRows && this.foundRowCount) return;
442 } else if (offset < lastOffset) {
443 if (offset > this.startPos+this.nearLimit) return;
444 if (this.startPos==0) return;
447 if (offset >= this.totalRows && this.foundRowCount) return;
449 this.processingRequest=true;
450 Rico.log("AjaxSQL fetch: processing offset="+offset);
451 var bufferStartPos = this.getFetchOffset(offset);
452 var fetchSize = this.getFetchSize(bufferStartPos);
453 var partialLoaded = false;
455 this.liveGrid.showMsg(this.options.waitMsg);
456 this.timeoutHandler = Rico.runLater(this.options.bufferTimeout, this, 'handleTimedOut');
457 this.ajaxOptions.parameters = this.formQueryHashSQL(bufferStartPos,fetchSize,this.options.fmt);
459 Rico.log('sending req #'+this.requestCount);
461 if (typeof this.dataSource=='string') {
462 this.ajaxOptions.onComplete = function(xhr) { self.ajaxUpdate(bufferStartPos, xhr); };
463 new Rico.ajaxRequest(this.dataSource, this.ajaxOptions);
465 this.ajaxOptions.onComplete = function(newRows, newStyle, totalRows, errMsg) { self.jsUpdate(bufferStartPos, newRows, newStyle, totalRows, errMsg); };
466 this.dataSource(this.ajaxOptions);
470 formQueryHashSQL: function(startPos,fetchSize,fmt) {
471 var queryHash=this.formQueryHashXML(startPos,fetchSize);
472 if (!this.foundRowCount) queryHash['get_total']='true';
473 if (fmt) queryHash._fmt=fmt;
476 Rico.extend(queryHash,this.sortParm);
479 for (var n=0; n<this.liveGrid.columns.length; n++) {
480 var c=this.liveGrid.columns[n];
481 if (c.filterType == Rico.ColumnConst.UNFILTERED) continue;
482 var colnum=typeof(c.format.filterCol)=='number' ? c.format.filterCol : c.index;
483 queryHash['f['+colnum+'][op]']=c.filterOp;
484 queryHash['f['+colnum+'][len]']=c.filterValues.length;
485 for (var i=0; i<c.filterValues.length; i++) {
486 var fval=c.filterValues[i];
487 if (c.format.type == 'date') {
488 var parts = fval.split('.');
489 if (parts.length > 1) {
491 for (var j=0; j < parts.length; j++)
492 if (parts[j].length == 1)
493 parts[j] = '0' + parts[j].toString();
494 fval = parts.join('-');
497 if (c.filterOp=='LIKE' && fval.indexOf('*')==-1) {
498 if (c.format.filterUI.charAt(1) == '^') fval=fval+'*';
499 else if (c.format.filterUI.charAt(1) == '$') fval='*'+fval;
500 else fval='*'+fval+'*';
502 queryHash['f['+colnum+']['+i+']']=fval;
508 getFetchSize: function(adjustedOffset) {
509 var adjustedSize = 0;
510 if (adjustedOffset >= this.startPos) { //appending
511 var endFetchOffset = this.maxFetchSize + adjustedOffset;
512 adjustedSize = endFetchOffset - adjustedOffset;
513 if(adjustedOffset == 0 && adjustedSize < this.maxFetchSize)
514 adjustedSize = this.maxFetchSize;
515 Rico.log("getFetchSize/append, adjustedSize="+adjustedSize+" adjustedOffset="+adjustedOffset+' endFetchOffset='+endFetchOffset);
516 } else { //prepending
517 adjustedSize = Math.min(this.startPos - adjustedOffset,this.maxFetchSize);
522 getFetchOffset: function(offset) {
523 var adjustedOffset = offset;
524 if (offset > this.startPos)
525 adjustedOffset = Math.max(offset, this.endPos()); //appending
526 else if (offset + this.maxFetchSize >= this.startPos)
527 adjustedOffset = Math.max(this.startPos - this.maxFetchSize, 0); //prepending
528 return adjustedOffset;
531 updateBuffer: function(start, newRows, newStyle) {
532 Rico.log("updateBuffer: start="+start+", # of rows="+this.rcvdRows);
533 if (this.rows.length == 0) { // initial load
535 this.attr = newStyle;
536 this.startPos = start;
537 } else if (start > this.startPos) { //appending
538 if (this.startPos + this.rows.length < start) {
540 this.attr = newStyle;
541 this.startPos = start;
543 this.rows = this.rows.concat( newRows.slice(0, newRows.length));
544 if (this.attr && newStyle) this.attr = this.attr.concat( newStyle.slice(0, newStyle.length));
545 if (this.rows.length > this.maxBufferSize) {
546 var fullSize = this.rows.length;
547 this.rows = this.rows.slice(this.rows.length - this.maxBufferSize, this.rows.length);
548 if (this.attr) this.attr = this.attr.slice(this.attr.length - this.maxBufferSize, this.attr.length);
549 this.startPos = this.startPos + (fullSize - this.rows.length);
552 } else { //prepending
553 if (start + newRows.length < this.startPos) {
555 this.attr = newStyle;
557 this.rows = newRows.slice(0, this.startPos).concat(this.rows);
558 if (newStyle) this.attr = newStyle.slice(0, this.startPos).concat(this.attr);
559 if (this.maxBufferSize && this.rows.length > this.maxBufferSize) {
560 this.rows = this.rows.slice(0, this.maxBufferSize);
561 if (this.attr) this.attr = this.attr.slice(0, this.maxBufferSize);
564 this.startPos = start;
566 this.size = this.rows.length;
569 sortBuffer: function(colnum) {
571 var col=this.liveGrid.columns[colnum];
572 if (this.options.sortParmFmt) {
573 this.sortParm['sort_col']=col[this.options.sortParmFmt];
574 this.sortParm['sort_dir']=col.getSortDirection();
576 this.sortParm['s'+colnum]=col.getSortDirection();
581 printAllSQL: function(exportType) {
582 var parms=this.formQueryHashSQL(0,this.liveGrid.options.maxPrint,exportType);
583 parms.hidden=this.liveGrid.listInvisible('index').join(',');
584 var url=this.dataSource+'?'+Rico.toQueryString(parms);
585 window.open(url,'',this.liveGrid.options.exportWindow);
588 printVisibleSQL: function(exportType) {
589 var parms=this.formQueryHashSQL(this.liveGrid.contentStartPos-1, this.liveGrid.pageSize, exportType);
590 parms.hidden=this.liveGrid.listInvisible('index').join(',');
591 var url=this.dataSource+'?'+Rico.toQueryString(parms);
592 window.open(url,'',this.liveGrid.options.exportWindow);
595 // for datasource that is a javascript function
596 _printAll: function() {
597 this.liveGrid.exportStart();
598 this.ajaxOptions.parameters = this.formQueryHashSQL(0,this.liveGrid.options.maxPrint);
600 this.ajaxOptions.onComplete = function() { self._jsExport(); };
601 this.dataSource(this.ajaxOptions);
604 _jsExport: function(newRows, newStyle, totalRows, errMsg) {
605 Rico.log("_jsExport: "+arguments.length);
607 Rico.log("_jsExport: received error="+errMsg);
608 this.liveGrid.showMsg(Rico.getPhraseById("requestError",errMsg));
611 this.exportBuffer(newRows,0);
612 this.liveGrid.exportFinish();