root/trunk/src/GridView.js

Revision 79, 61.9 KB (checked in by tsuckow, 12 months ago)
  • fixed: (GridView?.js) fixed an issue where selections not currently in the buffer would get accidently removed once the grid gets reset
  • enhancement: added CheckboxSelectionModel?.js to be able to use a checkboxselection with Ext.ux.Livegrid
Line 
1/**
2 * Ext.ux.grid.livegrid.GridView
3 * Copyright (c) 2007-2008, http://www.siteartwork.de
4 *
5 * Ext.ux.grid.livegrid.GridView is licensed under the terms of the
6 *                  GNU Open Source GPL 3.0
7 * license.
8 *
9 * Commercial use is prohibited. Visit <http://www.siteartwork.de/livegrid>
10 * if you need to obtain a commercial license.
11 *
12 * This program is free software: you can redistribute it and/or modify it under
13 * the terms of the GNU General Public License as published by the Free Software
14 * Foundation, either version 3 of the License, or any later version.
15 *
16 * This program is distributed in the hope that it will be useful, but WITHOUT
17 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
18 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
19 * details.
20 *
21 * You should have received a copy of the GNU General Public License along with
22 * this program. If not, see <http://www.gnu.org/licenses/gpl.html>.
23 *
24 */
25
26Ext.namespace('Ext.ux.grid.livegrid');
27
28/**
29 * @class Ext.ux.grid.livegrid.GridView
30 * @extends Ext.grid.GridView
31 * @constructor
32 * @param {Object} config
33 *
34 * @author Thorsten Suckow-Homberg <ts@siteartwork.de>
35 */
36Ext.ux.grid.livegrid.GridView = function(config) {
37
38    this.addEvents({
39        /**
40         * @event reset
41         * Fires when the grid resets.
42         * @param {Ext.ux.grid.livegrid.GridView} this
43         * @param {Boolean} forceReload
44         */
45        'reset' : true,
46        /**
47         * @event beforebuffer
48         * Fires when the store is about to buffer new data.
49         * @param {Ext.ux.BufferedGridView} this
50         * @param {Ext.data.Store} store The store
51         * @param {Number} rowIndex
52         * @param {Number} visibleRows
53         * @param {Number} totalCount
54         * @param {Number} options The options with which the buffer request was called
55         */
56        'beforebuffer' : true,
57        /**
58         * @event buffer
59         * Fires when the store is finsihed buffering new data.
60         * @param {Ext.ux.BufferedGridView} this
61         * @param {Ext.data.Store} store The store
62         * @param {Number} rowIndex
63         * @param {Number} visibleRows
64         * @param {Number} totalCount
65         * @param {Object} options
66         */
67        'buffer' : true,
68        /**
69         * @event bufferfailure
70         * Fires when buffering failed.
71         * @param {Ext.ux.BufferedGridView} this
72         * @param {Ext.data.Store} store The store
73         * @param {Object} options The options the buffer-request was initiated with
74         */
75        'bufferfailure' : true,
76        /**
77         * @event cursormove
78         * Fires when the the user scrolls through the data.
79         * @param {Ext.ux.BufferedGridView} this
80         * @param {Number} rowIndex The index of the first visible row in the
81         *                          grid absolute to it's position in the model.
82         * @param {Number} visibleRows The number of rows visible in the grid.
83         * @param {Number} totalCount
84         */
85        'cursormove' : true,
86        /**
87         * @event abortrequest
88         * Fires when the store is about to reload (this does NOT mean buffering).
89         * If you are using a custom proxy in your store, you should listen to this event
90         * and abort any ongoing server request established in your custom proxy.
91         * @param {Ext.data.Store} store
92         * @param {Object} options
93         */
94        'abortrequest' : true
95
96    });
97
98    /**
99     * @cfg {Number} scrollDelay The number of microseconds a call to the
100     * onLiveScroll-lisener should be delayed when the scroll event fires
101     */
102
103    /**
104     * @cfg {Number} bufferSize The number of records that will at least always
105     * be available in the store for rendering. This value will be send to the
106     * server as the <tt>limit</tt> parameter and should not change during the
107     * lifetime of a grid component. Note: In a paging grid, this number would
108     * indicate the page size.
109     * The value should be set high enough to make a userfirendly scrolling
110     * possible and should be greater than the sum of {nearLimit} and
111     * {visibleRows}. Usually, a value in between 150 and 200 is good enough.
112     * A lesser value will more often make the store re-request new data, while
113     * a larger number will make loading times higher.
114     */
115
116    /**
117     * @cfg {Number} nearLimit This value represents a near value that is responsible
118     * for deciding if a request for new data is needed. The lesser the number, the
119     * more often new data will be requested. The number should be set to a value
120     * that lies in between 1/4 to 1/2 of the {bufferSize}.
121     */
122
123    /**
124     * @cfg {Number} horizontalScrollOffset The height of a horizontal aligned
125     * scrollbar.  The scrollbar is shown if the total width of all visible
126     * columns exceeds the width of the grid component.
127     * On Windows XP (IE7, FF2), this value defaults to 17.
128     */
129    this.horizontalScrollOffset = 17;
130
131    /**
132     * @type {Boolean} _checkEmptyBody Since Ext 3.0, &nbsp; would initially added to the mainBody
133     * as the first child if there are no rows to render. This element has to be removed when
134     * the first rows get added so the UI does not crash. This property is here to determine if
135     * this element was already removed, so we don't have to query innerHTML all the time.
136     */
137    this._checkEmptyBody = true;
138
139    Ext.apply(this, config);
140
141    this.templates = {};
142    /**
143     * The master template adds an addiiotnal scrollbar to make cursoring in the
144     * data possible.
145     */
146    this.templates.master = new Ext.Template(
147        '<div class="x-grid3" hidefocus="true"><div class="liveScroller"><div></div><div></div><div></div></div>',
148            '<div class="x-grid3-viewport"">',
149                '<div class="x-grid3-header"><div class="x-grid3-header-inner"><div class="x-grid3-header-offset" style="{ostyle}">{header}</div></div><div class="x-clear"></div></div>',
150                '<div class="x-grid3-scroller" style="overflow-y:hidden !important;"><div class="x-grid3-body" style="{bstyle}">{body}</div><a href="#" class="x-grid3-focus" tabIndex="-1"></a></div>',
151            "</div>",
152            '<div class="x-grid3-resize-marker">&#160;</div>',
153            '<div class="x-grid3-resize-proxy">&#160;</div>',
154        "</div>"
155    );
156
157    // shorthands for often used parent classes
158    this._gridViewSuperclass = Ext.ux.grid.livegrid.GridView.superclass;
159
160    this._gridViewSuperclass.constructor.call(this);
161
162
163};
164
165
166Ext.extend(Ext.ux.grid.livegrid.GridView, Ext.grid.GridView, {
167
168// {{{ --------------------------properties-------------------------------------
169
170    /**
171     * Stores the height of the header. Needed for recalculating scroller inset height.
172     * @param {Number}
173     */
174    hdHeight : 0,
175
176    /**
177     * Indicates wether the last row in the grid is clipped and thus not fully display.
178     * 1 if clipped, otherwise 0.
179     * @param {Number}
180     */
181    rowClipped : 0,
182
183
184    /**
185     * This is the actual y-scroller that does control sending request to the server
186     * based upon the position of the scrolling cursor.
187     * @param {Ext.Element}
188     */
189    liveScroller : null,
190
191    /**
192     * This array holds the divs that represent the amount of data in a given repository.
193     * The sum of heights of this divs gets computed via the total amount of records
194     * multiplied with the fixed(!) row height.
195     * There is a total of 3 divs responsible for the scroll amount to prevent issues
196     * with the max number of pxiels a div alone can grow in height.
197     * @param {native HTMLObject}
198     */
199    liveScrollerInsets : null,
200
201    /**
202     * The <b>fixed</b> row height for <b>every</b> row in the grid. The value is
203     * computed once the store has been loaded for the first time and used for
204     * various calculations during the lifetime of the grid component, such as
205     * the height of the scroller and the number of visible rows.
206     * @param {Number}
207     */
208    rowHeight : -1,
209
210    /**
211     * Stores the number of visible rows that have to be rendered.
212     * @param {Number}
213     */
214    visibleRows : 1,
215
216    /**
217     * Stores the last offset relative to a previously scroll action. This is
218     * needed for deciding wether the user scrolls up or down.
219     * @param {Number}
220     */
221    lastIndex : -1,
222
223    /**
224     * Stores the last visible row at position "0" in the table view before
225     * a new scroll event was created and fired.
226     * @param {Number}
227     */
228    lastRowIndex : 0,
229
230    /**
231     * Stores the value of the <tt>liveScroller</tt>'s <tt>scrollTop</tt> DOM
232     * property.
233     * @param {Number}
234     */
235    lastScrollPos : 0,
236
237    /**
238     * The current index of the row in the model that is displayed as the first
239     * visible row in the view.
240     * @param {Number}
241     */
242    rowIndex : 0,
243
244    /**
245    * Set to <tt>true</tt> if the store is busy with loading new data.
246    * @param {Boolean}
247    */
248    isBuffering : false,
249
250        /**
251         * If a request for new data was made and the user scrolls to a new position
252         * that lays not within the requested range of the new data, the queue will
253         * hold the latest requested position. If the buffering succeeds and the value
254         * of requestQueue is not within the range of the current buffer, data may be
255         * re-requested.
256         *
257         * @param {Number}
258         */
259    requestQueue : -1,
260
261    /**
262     * An {@Ext.LoadMask} config that will be shown when a request to data was made
263     * and there are no rows in the buffer left to render.
264     * @param {Object}
265     */
266    loadMask : false,
267
268    /**
269     * A shortcut to indicate whether the loadMask is currently being displayed.
270     * @type {Boolean}
271     * @private
272     */
273    loadMaskDisplayed : false,
274
275    /**
276     * Set to <tt>true</tt> if a request for new data has been made while there
277     * are still rows in the buffer that can be rendered before the request
278     * finishes.
279     * @param {Boolean}
280     */
281    isPrebuffering : false,
282
283    /**
284     * The dom node for which the node mask will be rendered.
285     * @type {Ext.Element}
286     * @private
287     */
288    _loadMaskAnchor : null,
289
290// }}}
291
292// {{{ --------------------------public API methods-----------------------------
293
294    /**
295     * Resets the view to display the first row in the data model. This will
296     * change the scrollTop property of the scroller and may trigger a request
297     * to buffer new data, if the row index "0" is not within the buffer range and
298     * forceReload is set to true.
299     *
300     * @param {Boolean} forceReload <tt>true</tt> to reload the buffers contents,
301     *                              othwerwise <tt>false</tt>
302     *
303     * @return {Boolean} Whether the store loads after reset(true); returns false
304     * if any of the attached beforeload listeners cancels the load-event
305     */
306    reset : function(forceReload)
307    {
308        if (forceReload === false) {
309            this.ds.modified = [];
310            //this.grid.selModel.clearSelections(true);
311            this.rowIndex      = 0;
312            this.lastScrollPos = 0;
313            this.lastRowIndex = 0;
314            this.lastIndex    = 0;
315            this.adjustVisibleRows();
316            this.adjustScrollerPos(-this.liveScroller.dom.scrollTop, true);
317            this.showLoadMask(false);
318
319            var _ofn = this.processRows;
320            this.processRows = Ext.emptyFn;
321            this.suspendEvents();
322            this.refresh(true);
323            this.resumeEvents();
324            this.processRows = _ofn;
325            this.processRows(0);
326
327            this.fireEvent('cursormove', this, 0,
328                           Math.min(this.ds.totalLength, this.visibleRows-this.rowClipped),
329                           this.ds.totalLength);
330            this.fireEvent('reset', this, forceReload);
331            return false;
332        } else {
333
334            var params = {};
335            var sInfo = this.ds.sortInfo;
336
337            if (sInfo) {
338                params = {
339                    dir  : sInfo.direction,
340                    sort : sInfo.field
341                };
342            }
343
344            this.fireEvent('reset', this, forceReload);
345            return this.ds.load({params : params});
346        }
347
348    },
349
350// {{{ ------------adjusted methods for applying custom behavior----------------
351
352    /**
353     * Overwritten so the {@link Ext.ux.grid.livegrid.DragZone} can be used
354     * with this view implementation.
355     *
356     * Since detaching a previously created DragZone from a grid panel seems to
357     * be impossible, a little workaround will tell the parent implementation
358     * that drad/drop is not enabled for this view's grid, and right after that
359     * the custom DragZone will be created, if neccessary.
360     */
361    renderUI : function()
362    {
363        var g = this.grid;
364        var dEnabled = g.enableDragDrop || g.enableDrag;
365
366        g.enableDragDrop = false;
367        g.enableDrag     = false;
368
369        var m = this._gridViewSuperclass.renderUI.call(this);
370
371        var g = this.grid;
372
373        g.enableDragDrop = dEnabled;
374        g.enableDrag     = dEnabled;
375
376        if(dEnabled){
377            this.dragZone = new Ext.ux.grid.livegrid.DragZone(g, {
378                ddGroup : g.ddGroup || 'GridDD'
379            });
380        }
381
382        return m;
383    },
384
385    afterRenderUI : function()
386    {
387        this._gridViewSuperclass.afterRenderUI.call(this);
388
389        if (this.loadMask) {
390            this._loadMaskAnchor = Ext.get(this.mainBody.dom.parentNode.parentNode);
391            Ext.apply(this.loadMask,{
392                msgCls : 'x-mask-loading'
393            });
394            this._loadMaskAnchor.mask(
395                this.loadMask.msg, this.loadMask.msgCls
396            );
397            var dom  = this._loadMaskAnchor.dom;
398            var data = Ext.Element.data;
399            data(dom, 'mask').addClass('ext-ux-livegrid');
400            data(dom, 'mask').setDisplayed(false);
401            data(dom, 'maskMsg').setDisplayed(false);
402        }
403    },
404
405    /**
406     * The extended implementation attaches an listener to the beforeload
407     * event of the store of the grid. It is guaranteed that the listener will
408     * only be executed upon reloading of the store, sorting and initial loading
409     * of data. When the store does "buffer", all events are suspended and the
410     * beforeload event will not be triggered.
411     *
412     * @param {Ext.grid.GridPanel} grid The grid panel this view is attached to
413     */
414    init: function(grid)
415    {
416        this._gridViewSuperclass.init.call(this, grid);
417
418        grid.on('expand', this._onExpand, this);
419    },
420
421    initData : function(ds, cm)
422    {
423        if(this.ds){
424            this.ds.un('bulkremove', this.onBulkRemove, this);
425            this.ds.un('beforeload', this.onBeforeLoad, this);
426        }
427        if(ds){
428            ds.on('bulkremove', this.onBulkRemove, this);
429            ds.on('beforeload', this.onBeforeLoad, this);
430        }
431
432        this._gridViewSuperclass.initData.call(this, ds, cm);
433    },
434
435    /**
436     * Only render the viewable rect of the table. The number of rows visible to
437     * the user is defined in <tt>visibleRows</tt>.
438     * This implementation does completely overwrite the parent's implementation.
439     */
440    // private
441    renderBody : function()
442    {
443        var markup = this.renderRows(0, this.visibleRows-1);
444        return this.templates.body.apply({rows: markup});
445    },
446
447    /**
448     * Overriden so the renderer of the specific cells gets the index of the
449     * row as available in the view passed (row's rowIndex property)-
450     *
451     */
452    doRender : function(cs, rs, ds, startRow, colCount, stripe)
453    {
454        return this._gridViewSuperclass.doRender.call(
455            this, cs, rs, ds, startRow + this.ds.bufferRange[0], colCount, stripe
456        );
457
458    },
459
460    /**
461     * Inits the DOM native elements for this component.
462     * The properties <tt>liveScroller</tt> and <tt>liveScrollerInsets</tt> will
463     * be respected as provided by the master template.
464     * The <tt>scroll</tt> listener for the <tt>liverScroller</tt> will also be
465     * added here as the <tt>mousewheel</tt> listener.
466     * This method overwrites the parents implementation.
467     */
468    // private
469    initElements : function()
470    {
471        var E = Ext.Element;
472
473        var el = this.grid.getGridEl().dom.firstChild;
474            var cs = el.childNodes;
475
476            this.el = new E(el);
477
478        this.mainWrap = new E(cs[1]);
479
480        // liveScroller and liveScrollerInsets
481        this.liveScroller       = new E(cs[0]);
482        var f = this.liveScroller.dom.firstChild;
483        this.liveScrollerInsets  = [
484            f,
485            f.nextSibling,
486            f.nextSibling.nextSibling
487        ];
488        this.liveScroller.on('scroll', this.onLiveScroll,  this, {buffer : this.scrollDelay});
489
490        var thd = this.mainWrap.dom.firstChild;
491            this.mainHd = new E(thd);
492
493            this.hdHeight = thd.offsetHeight;
494
495            this.innerHd = this.mainHd.dom.firstChild;
496        this.scroller = new E(this.mainWrap.dom.childNodes[1]);
497        if(this.forceFit){
498            this.scroller.setStyle('overflow-x', 'hidden');
499        }
500        this.mainBody = new E(this.scroller.dom.firstChild);
501
502        // addd the mousewheel event to the table's body
503        this.mainBody.on('mousewheel', this.handleWheel,  this);
504
505            this.focusEl = new E(this.scroller.dom.childNodes[1]);
506        this.focusEl.swallowEvent("click", true);
507
508        this.resizeMarker = new E(cs[2]);
509        this.resizeProxy = new E(cs[3]);
510
511    },
512
513        /**
514         * Layouts the grid's view taking the scroller into account. The height
515         * of the scroller gets adjusted depending on the total width of the columns.
516         * The width of the grid view will be adjusted so the header and the rows do
517         * not overlap the scroller.
518         * This method will also compute the row-height based on the first row this
519         * grid displays and will adjust the number of visible rows if a resize
520         * of the grid component happened.
521         * This method overwrites the parents implementation.
522         */
523        //private
524    layout : function()
525    {
526        if(!this.mainBody){
527            return; // not rendered
528        }
529        var g = this.grid;
530        var c = g.getGridEl(), cm = this.cm,
531                expandCol = g.autoExpandColumn,
532                gv = this;
533
534        var csize = c.getSize(true);
535
536        // set vw to 19 to take scrollbar width into account!
537        var vw = csize.width;
538
539        if(!g.hideHeaders && vw < 20 || csize.height < 20){ // display: none?
540            return;
541        }
542
543        if(g.autoHeight){
544            this.scroller.dom.style.overflow = 'visible';
545            if(Ext.isWebKit){
546                this.scroller.dom.style.position = 'static';
547            }
548        }else{
549            this.el.setSize(csize.width, csize.height);
550
551            var hdHeight = this.mainHd.getHeight();
552            var vh = csize.height - (hdHeight);
553
554            this.scroller.setSize(vw, vh);
555            if(this.innerHd){
556                this.innerHd.style.width = (vw)+'px';
557            }
558        }
559
560        this.liveScroller.dom.style.top = this.hdHeight+"px";
561
562        if(this.forceFit){
563            if(this.lastViewWidth != vw){
564                this.fitColumns(false, false);
565                this.lastViewWidth = vw;
566            }
567        }else {
568            this.autoExpand();
569        }
570
571        // adjust the number of visible rows and the height of the scroller.
572        this.adjustVisibleRows();
573        this.adjustBufferInset();
574
575        this.onLayout(vw, vh);
576    },
577
578    /**
579     * Overriden for Ext 2.2 to prevent call to focus Row.
580     *
581     */
582    removeRow : function(row)
583    {
584        Ext.removeNode(this.getRow(row));
585    },
586
587    /**
588     * Overriden for Ext 2.2 to prevent call to focus Row.
589     * This method i s here for dom operations only - the passed arguments are the
590     * index of the nodes in the dom, not in the model.
591     *
592     */
593    removeRows : function(firstRow, lastRow)
594    {
595        var bd = this.mainBody.dom;
596        for(var rowIndex = firstRow; rowIndex <= lastRow; rowIndex++){
597            Ext.removeNode(bd.childNodes[firstRow]);
598        }
599    },
600
601// {{{ ----------------------dom/mouse listeners--------------------------------
602
603    /**
604     * Tells the view to recalculate the number of rows displayable
605     * and the buffer inset, when it gets expanded after it has been
606     * collapsed.
607     *
608     */
609    _onExpand : function(panel)
610    {
611        this.adjustVisibleRows();
612        this.adjustBufferInset();
613        this.adjustScrollerPos(this.rowHeight*this.rowIndex, true);
614    },
615
616    // private
617    onColumnMove : function(cm, oldIndex, newIndex)
618    {
619        this.indexMap = null;
620        this.replaceLiveRows(this.rowIndex, true);
621        this.updateHeaders();
622        this.updateHeaderSortState();
623        this.afterMove(newIndex);
624        this.grid.fireEvent('columnmove', oldIndex, newIndex);
625    },
626
627
628    /**
629     * Called when a column width has been updated. Adjusts the scroller height
630     * and the number of visible rows wether the horizontal scrollbar is shown
631     * or not.
632     */
633    onColumnWidthUpdated : function(col, w, tw)
634    {
635        this.adjustVisibleRows();
636        this.adjustBufferInset();
637    },
638
639    /**
640     * Called when the width of all columns has been updated. Adjusts the scroller
641     * height and the number of visible rows wether the horizontal scrollbar is shown
642     * or not.
643     */
644    onAllColumnWidthsUpdated : function(ws, tw)
645    {
646        this.adjustVisibleRows();
647        this.adjustBufferInset();
648    },
649
650    /**
651     * Callback for selecting a row. The index of the row is the absolute index
652     * in the datamodel. If the row is not rendered, this method will do nothing.
653     */
654    // private
655    onRowSelect : function(row)
656    {
657        if (row < this.rowIndex || row > this.rowIndex+this.visibleRows) {
658            return;
659        }
660
661        this.addRowClass(row, this.selectedRowClass);
662    },
663
664    /**
665     * Callback for deselecting a row. The index of the row is the absolute index
666     * in the datamodel. If the row is not currently rendered in the view, this method
667     * will do nothing.
668     */
669    // private
670    onRowDeselect : function(row)
671    {
672        if (row < this.rowIndex || row > this.rowIndex+this.visibleRows) {
673            return;
674        }
675
676        this.removeRowClass(row, this.selectedRowClass);
677    },
678
679
680// {{{ ----------------------data listeners-------------------------------------
681    /**
682     * Called when the buffer gets cleared. Simply calls the updateLiveRows method
683     * with the adjusted index and should force the store to reload
684     */
685    // private
686    onClear : function()
687    {
688        this.reset(false);
689    },
690
691    /**
692     * Callback for the "bulkremove" event of the attached datastore.
693     *
694     * @param {Ext.ux.grid.livegrid.Store} store
695     * @param {Array} removedData
696     *
697     */
698    onBulkRemove : function(store, removedData)
699    {
700        var record    = null;
701        var index     = 0;
702        var viewIndex = 0;
703        var len       = removedData.length;
704
705        var removedInView    = false;
706        var removedAfterView = false;
707        var scrollerAdjust   = 0;
708
709        if (len == 0) {
710            return;
711        }
712
713        var tmpRowIndex   = this.rowIndex;
714        var removedBefore = 0;
715        var removedAfter  = 0;
716        var removedIn     = 0;
717
718        for (var i = 0; i < len; i++) {
719            record = removedData[i][0];
720            index  = removedData[i][1];
721
722            viewIndex = (index != Number.MIN_VALUE && index != Number.MAX_VALUE)
723                      ? index + this.ds.bufferRange[0]
724                      : index;
725
726            if (viewIndex < this.rowIndex) {
727                removedBefore++;
728            } else if (viewIndex >= this.rowIndex && viewIndex <= this.rowIndex+(this.visibleRows-1)) {
729                removedIn++;
730            } else if (viewIndex >= this.rowIndex+this.visibleRows) {
731                removedAfter++;
732            }
733
734            this.fireEvent("beforerowremoved", this, viewIndex, record);
735            this.fireEvent("rowremoved",       this, viewIndex, record);
736        }
737
738        var totalLength = this.ds.totalLength;
739        this.rowIndex   = Math.max(0, Math.min(this.rowIndex - removedBefore, totalLength-(this.visibleRows-1)));
740
741        this.lastRowIndex = this.rowIndex;
742
743        this.adjustScrollerPos(-(removedBefore*this.rowHeight), true);
744        this.updateLiveRows(this.rowIndex, true);
745        this.adjustBufferInset();
746        this.processRows(0, undefined, false);
747
748    },
749
750
751    /**
752     * Callback for the underlying store's remove method. The current
753     * implementation does only remove the selected row which record is in the
754     * current store.
755     *
756     * @see onBulkRemove()
757     */
758    // private
759    onRemove : function(ds, record, index)
760    {
761        this.onBulkRemove(ds, [[record, index]]);
762    },
763
764    /**
765     * The callback for the underlying data store when new data was added.
766     * If <tt>index</tt> equals to <tt>Number.MIN_VALUE</tt> or <tt>Number.MAX_VALUE</tt>, the
767     * method can't tell at which position in the underlying data model the
768     * records where added. However, if <tt>index</tt> equals to <tt>Number.MIN_VALUE</tt>,
769     * the <tt>rowIndex</tt> property will be adjusted to <tt>rowIndex+records.length</tt>,
770     * and the <tt>liveScroller</tt>'s properties get adjusted so it matches the
771     * new total number of records of the underlying data model.
772     * The same will happen to any records that get added at the store index which
773     * is currently represented by the first visible row in the view.
774     * Any other value will cause the method to compute the number of rows that
775     * have to be (re-)painted and calling the <tt>insertRows</tt> method, if
776     * neccessary.
777     *
778     * This method triggers the <tt>beforerowsinserted</tt> and <tt>rowsinserted</tt>
779     * event, passing the indexes of the records as they may default to the
780     * positions in the underlying data model. However, due to the fact that
781     * any sort algorithm may have computed the indexes of the records, it is
782     * not guaranteed that the computed indexes equal to the indexes of the
783     * underlying data model.
784     *
785     * @param {Ext.ux.grid.livegrid.Store} ds The datastore that buffers records
786     *                                       from the underlying data model
787     * @param {Array} records An array containing the newly added
788     *                        {@link Ext.data.Record}s
789     * @param {Number} index The index of the position in the underlying
790     *                       {@link Ext.ux.grid.livegrid.Store} where the rows
791     *                       were added.
792     */
793    // private
794    onAdd : function(ds, records, index)
795    {
796        if (this._checkEmptyBody) {
797            if (this.mainBody.dom.innerHTML == '&nbsp;') {
798                this.mainBody.dom.innerHTML = '';
799            }
800            this._checkEmptyBody = false;
801        }
802
803        var recordLen = records.length;
804
805        // values of index which equal to Number.MIN_VALUE or Number.MAX_VALUE
806        // indicate that the records were not added to the store. The component
807        // does not know which index those records do have in the underlying
808        // data model
809        if (index == Number.MAX_VALUE || index == Number.MIN_VALUE) {
810            this.fireEvent("beforerowsinserted", this, index, index);
811
812            // if index equals to Number.MIN_VALUE, shift rows!
813            if (index == Number.MIN_VALUE) {
814
815                this.rowIndex     = this.rowIndex + recordLen;
816                this.lastRowIndex = this.rowIndex;
817
818                this.adjustBufferInset();
819                this.adjustScrollerPos(this.rowHeight*recordLen, true);
820
821                this.fireEvent("rowsinserted", this, index, index, recordLen);
822                this.processRows(0, undefined, false);
823                // the cursor did virtually move
824                this.fireEvent('cursormove', this, this.rowIndex,
825                               Math.min(this.ds.totalLength, this.visibleRows-this.rowClipped),
826                               this.ds.totalLength);
827
828                return;
829            }
830
831            this.adjustBufferInset();
832            this.fireEvent("rowsinserted", this, index, index, recordLen);
833            return;
834        }
835
836        // only insert the rows which affect the current view.
837        var start = index+this.ds.bufferRange[0];
838        var end   = start + (recordLen-1);
839        var len   = this.getRows().length;
840
841        var firstRow = 0;
842        var lastRow  = 0;
843
844        // rows would be added at the end of the rows which are currently
845        // displayed, so fire the event, resize buffer and adjust visible
846        // rows and return
847        if (start > this.rowIndex+(this.visibleRows-1)) {
848            this.fireEvent("beforerowsinserted", this, start, end);
849            this.fireEvent("rowsinserted",       this, start, end, recordLen);
850
851            this.adjustVisibleRows();
852            this.adjustBufferInset();
853
854        }
855
856        // rows get added somewhere in the current view.
857        else if (start >= this.rowIndex && start <= this.rowIndex+(this.visibleRows-1)) {
858            firstRow = index;
859            // compute the last row that would be affected of an insert operation
860            lastRow  = index+(recordLen-1);
861            this.lastRowIndex  = this.rowIndex;
862            this.rowIndex      = (start > this.rowIndex) ? this.rowIndex : start;
863
864            this.insertRows(ds, firstRow, lastRow);
865
866            if (this.lastRowIndex != this.rowIndex) {
867                this.fireEvent('cursormove', this, this.rowIndex,
868                               Math.min(this.ds.totalLength, this.visibleRows-this.rowClipped),
869                               this.ds.totalLength);
870            }
871
872            this.adjustVisibleRows();
873            this.adjustBufferInset();
874        }
875
876        // rows get added before the first visible row, which would not affect any
877        // rows to be re-rendered
878        else if (start < this.rowIndex) {
879            this.fireEvent("beforerowsinserted", this, start, end);
880
881            this.rowIndex     = this.rowIndex+recordLen;
882            this.lastRowIndex = this.rowIndex;
883
884            this.adjustVisibleRows();
885            this.adjustBufferInset();
886
887            this.adjustScrollerPos(this.rowHeight*recordLen, true);
888
889            this.fireEvent("rowsinserted", this, start, end, recordLen);
890            this.processRows(0, undefined, true);
891
892            this.fireEvent('cursormove', this, this.rowIndex,
893                           Math.min(this.ds.totalLength, this.visibleRows-this.rowClipped),
894                           this.ds.totalLength);
895        }
896
897
898
899
900    },
901
902// {{{ ----------------------store listeners------------------------------------
903    /**
904     * This callback for the store's "beforeload" event will adjust the start
905     * position and the limit of the data in the model to fetch. It is guaranteed
906     * that this method will only be called when the store initially loads,
907     * remeote-sorts or reloads.
908     * All other load events will be suspended when the view requests buffer data.
909     * See {updateLiveRows}.
910     * Note:
911     * If you are using a custom proxy, such as {Ext.data.DirectProxy}, you should listen
912     * to the 'abortrequest'-event, which will tell that an ongoing "read" request should be
913     * aborted, since the grid's store gets refreshed.
914     * If the store is using an instance of {Ext.data.HttpProxy}, the method will still be
915     * fired, but the request made through this proxy will be aborted automatically.
916     *
917     *
918     * @param {Ext.data.Store} store The store the Grid Panel uses
919     * @param {Object} options The configuration object for the proxy that loads
920     *                         data from the server
921     */
922    onBeforeLoad : function(store, options)
923    {
924        var proxy = store.proxy;
925        if (proxy.activeRequest && proxy.activeRequest[Ext.data.Api.actions.read]) {
926            proxy.getConnection().abort(proxy.activeRequest[Ext.data.Api.actions.read]);
927        }
928        this.fireEvent('abortrequest', store, options);
929
930        this.isBuffering    = false;
931        this.isPreBuffering = false;
932
933        options.params = options.params || {};
934
935        var apply = Ext.apply;
936
937        apply(options, {
938            scope    : this,
939            callback : function(){
940                this.reset(false);
941            },
942            suspendLoadEvent : false
943        });
944
945        apply(options.params, {
946            start    : 0,
947            limit    : this.ds.bufferSize
948        });
949
950        return true;
951    },
952
953    /**
954     * Method is used as a callback for the load-event of the attached data store.
955     * Adjusts the buffer inset based upon the <tt>totalCount</tt> property
956     * returned by the response.
957     * Overwrites the parent's implementation.
958     */
959    onLoad : function(o1, o2, options)
960    {
961        this.adjustBufferInset();
962    },
963
964    /**
965     * This will be called when the data in the store has changed, i.e. a
966     * re-buffer has occured. If the table was not rendered yet, a call to
967     * <tt>refresh</tt> will initially render the table, which DOM elements will
968     * then be used to re-render the table upon scrolling.
969     *
970     */
971    // private
972    onDataChange : function(store)
973    {
974        this.updateHeaderSortState();
975    },
976
977    /**
978     * A callback for the store when new data has been buffered successfully.
979     * If the current row index is not within the range of the newly created
980     * data buffer or another request to new data has been made while the store
981     * was loading, new data will be re-requested.
982     *
983     * Additionally, if there are any rows that have been selected which were not
984     * in the data store, the method will request the pending selections from
985     * the grid's selection model and add them to the selections if available.
986     * This is because the component assumes that a user who scrolls through the
987     * rows and updates the view's buffer during scrolling, can check the selected
988     * rows which come into the view for integrity. It is up to the user to
989     * deselect those rows not matchuing the selection.
990     * Additionally, if the version of the store changes during various requests
991     * and selections are still pending, the versionchange event of the store
992     * can delete the pending selections after a re-bufer happened and before this
993     * method was called.
994     *
995     */
996    // private
997    liveBufferUpdate : function(records, options, success)
998    {
999        if (success === true) {
1000            this.adjustBufferInset();
1001
1002            this.fireEvent('buffer', this, this.ds, this.rowIndex,
1003                Math.min(this.ds.totalLength, this.visibleRows-this.rowClipped),
1004                this.ds.totalLength,
1005                options
1006            );
1007
1008            // this is needed since references to records which have been unloaded
1009            // get lost when the store gets loaded with new data.
1010            // from the store
1011            this.grid.selModel.replaceSelections(records);
1012
1013            this.isBuffering    = false;
1014            this.isPrebuffering = false;
1015            this.showLoadMask(false);
1016
1017            if (this.requestQueue >= 0) {
1018                var offset = this.requestQueue;
1019                this.requestQueue = -1;
1020                this.updateLiveRows(offset);
1021                return;
1022            }
1023
1024            if (this.isInRange(this.rowIndex)) {
1025                this.replaceLiveRows(this.rowIndex, options.forceRepaint);
1026            } else {
1027                this.updateLiveRows(this.rowIndex);
1028            }
1029
1030
1031            return;
1032        } else {
1033            this.fireEvent('bufferfailure', this, this.ds, options);
1034        }
1035
1036        this.requestQueue   = -1;
1037        this.isBuffering    = false;
1038        this.isPrebuffering = false;
1039        this.showLoadMask(false);
1040    },
1041
1042
1043// {{{ ----------------------scroll listeners------------------------------------
1044    /**
1045     * Handles mousewheel event on the table's body. This is neccessary since the
1046     * <tt>liveScroller</tt> element is completely detached from the table's body.
1047     *
1048     * @param {Ext.EventObject} e The event object
1049     */
1050    handleWheel : function(e)
1051    {
1052        if (this.rowHeight == -1) {
1053            e.stopEvent();
1054            return;
1055        }
1056        var d = e.getWheelDelta();
1057
1058        this.adjustScrollerPos(-(d*this.rowHeight));
1059
1060        e.stopEvent();
1061    },
1062
1063    /**
1064     * Handles scrolling through the grid. Since the grid is fixed and rows get
1065     * removed/ added subsequently, the only way to determine the actual row in
1066     * view is to measure the <tt>scrollTop</tt> property of the <tt>liveScroller</tt>'s
1067     * DOM element.
1068     *
1069     */
1070    onLiveScroll : function()
1071    {
1072        var scrollTop = this.liveScroller.dom.scrollTop;
1073
1074        var cursor = Math.floor((scrollTop)/this.rowHeight);
1075
1076        this.rowIndex = cursor;
1077        // the lastRowIndex will be set when refreshing the view has finished
1078        if (cursor == this.lastRowIndex) {
1079            return;
1080        }
1081
1082        this.updateLiveRows(cursor);
1083
1084        this.lastScrollPos = this.liveScroller.dom.scrollTop;
1085    },
1086
1087
1088
1089// {{{ --------------------------helpers----------------------------------------
1090
1091    // private
1092    refreshRow : function(record)
1093    {
1094        var ds = this.ds, index;
1095        if(typeof record == 'number'){
1096            index = record;
1097            record = ds.getAt(index);
1098        }else{
1099            index = ds.indexOf(record);
1100        }
1101
1102        var viewIndex = index + this.ds.bufferRange[0];
1103
1104        if (viewIndex < this.rowIndex || viewIndex >= this.rowIndex + this.visibleRows) {
1105            this.fireEvent("rowupdated", this, viewIndex, record);
1106            return;
1107        }
1108
1109        this.insertRows(ds, index, index, true);
1110        this.fireEvent("rowupdated", this, viewIndex, record);
1111    },
1112
1113
1114    /**
1115     * Overwritten so the rowIndex can be changed to the absolute index.
1116     *
1117     * If the third parameter equals to <tt>true</tt>, the method will also
1118     * repaint the selections.
1119     */
1120    // private
1121    processRows : function(startRow, skipStripe, paintSelections)
1122    {
1123        if(!this.ds || this.ds.getCount() < 1){
1124            return;
1125        }
1126
1127        skipStripe = skipStripe || !this.grid.stripeRows;
1128
1129        var cursor      = this.rowIndex;
1130        var rows        = this.getRows();
1131        var index       = 0;
1132        var sm          = this.grid.selModel;
1133        var allSelected = sm.isAllSelected();
1134
1135        var row = null;
1136        for (var idx = 0, len = rows.length; idx < len; idx++) {
1137            row = rows[idx];
1138
1139            row.rowIndex = index = cursor+idx;
1140            row.className = row.className.replace(this.rowClsRe, ' ');
1141            if (!skipStripe && (index + 1) % 2 === 0) {
1142                row.className += ' x-grid3-row-alt';
1143            }
1144
1145            if (paintSelections !== false) {
1146                if (sm.isSelected(this.ds.getAt(index)) === true) {
1147                    this.addRowClass(index, this.selectedRowClass);
1148                } else {
1149                    this.removeRowClass(index, this.selectedRowClass);
1150                }
1151                this.fly(row).removeClass("x-grid3-row-over");
1152            }
1153        }
1154
1155        // add first/last-row classes
1156        if(cursor === 0){
1157            Ext.fly(rows[0]).addClass(this.firstRowCls);
1158        } else if (cursor + rows.length == this.ds.totalLength) {
1159            Ext.fly(rows[rows.length - 1]).addClass(this.lastRowCls);
1160        }
1161    },
1162
1163    /**
1164     * API only, since the passed arguments are the indexes in the buffer store.
1165     * However, the method will try to compute the indexes so they might match
1166     * the indexes of the records in the underlying data model.
1167     *
1168     */
1169    // private
1170    insertRows : function(dm, firstRow, lastRow, isUpdate)
1171    {
1172        var viewIndexFirst = firstRow + this.ds.bufferRange[0];
1173        var viewIndexLast  = lastRow  + this.ds.bufferRange[0];
1174
1175        if (!isUpdate) {
1176            this.fireEvent("beforerowsinserted", this, viewIndexFirst, viewIndexLast);
1177        }
1178
1179        // first off, remove the rows at the bottom of the view to match the
1180        // visibleRows value and to not cause any spill in the DOM
1181        if (isUpdate !== true && (this.getRows().length + (lastRow-firstRow)) >= this.visibleRows) {
1182            this.removeRows((this.visibleRows-1)-(lastRow-firstRow), this.visibleRows-1);
1183        } else if (isUpdate) {
1184            this.removeRows(viewIndexFirst-this.rowIndex, viewIndexLast-this.rowIndex);
1185        }
1186
1187        // compute the range of possible records which could be drawn into the view without
1188        // causing any spill
1189        var lastRenderRow = (firstRow == lastRow)
1190                          ? lastRow
1191                          : Math.min(lastRow,  (this.rowIndex-this.ds.bufferRange[0])+(this.visibleRows-1));
1192
1193        var html = this.renderRows(firstRow, lastRenderRow);
1194
1195        var before = this.getRow(viewIndexFirst);
1196
1197        if (before) {
1198            Ext.DomHelper.insertHtml('beforeBegin', before, html);
1199        } else {
1200            Ext.DomHelper.insertHtml('beforeEnd', this.mainBody.dom, html);
1201        }
1202
1203        // if a row is replaced, we need to set the row index for this
1204        // row
1205        if (isUpdate === true) {
1206            var rows   = this.getRows();
1207            var cursor = this.rowIndex;
1208            for (var i = 0, max_i = rows.length; i < max_i; i++) {
1209                rows[i].rowIndex = cursor+i;
1210            }
1211        }
1212
1213        if (!isUpdate) {
1214            this.fireEvent("rowsinserted", this, viewIndexFirst, viewIndexLast, (viewIndexLast-viewIndexFirst)+1);
1215            this.processRows(0, undefined, true);
1216        }
1217    },
1218
1219    /**
1220     * Return the <TR> HtmlElement which represents a Grid row for the specified index.
1221     * The passed argument is assumed to be the absolute index and will get translated
1222     * to the index of the row that represents the data in the view.
1223     *
1224     * @param {Number} index The row index
1225     *
1226     * @return {null|HtmlElement} The <TR> element, or null if the row is not rendered
1227     * in the view.
1228     */
1229    getRow : function(row)
1230    {
1231        if (row-this.rowIndex < 0) {
1232            return null;
1233        }
1234
1235        return this.getRows()[row-this.rowIndex];
1236    },
1237
1238    /**
1239     * Returns the grid's <TD> HtmlElement at the specified coordinates.
1240     * Returns null if the specified row is not currently rendered.
1241     *
1242     * @param {Number} row The row index in which to find the cell.
1243     * @param {Number} col The column index of the cell.
1244     * @return {HtmlElement} The &lt;TD> at the specified coordinates.
1245     */
1246    getCell : function(row, col)
1247    {
1248        var row = this.getRow(row);
1249
1250        return row
1251               ? row.getElementsByTagName('td')[col]
1252               : null;
1253    },
1254
1255    /**
1256     * Focuses the specified cell.
1257     * @param {Number} row The row index
1258     * @param {Number} col The column index
1259     */
1260    focusCell : function(row, col, hscroll)
1261    {
1262        var xy = this.ensureVisible(row, col, hscroll);
1263
1264        if (!xy) {
1265                return;
1266                }
1267
1268                this.focusEl.setXY(xy);
1269
1270        if(Ext.isGecko){
1271            this.focusEl.focus();
1272        }else{
1273            this.focusEl.focus.defer(1, this.focusEl);
1274        }
1275
1276    },
1277
1278    /**
1279     * Makes sure that the requested /row/col is visible in the viewport.
1280     * The method may invoke a request for new buffer data and triggers the
1281     * scroll-event of the <tt>liveScroller</tt> element.
1282     *
1283     */
1284    // private
1285    ensureVisible : function(row, col, hscroll)
1286    {
1287        if(typeof row != "number"){
1288            row = row.rowIndex;
1289        }
1290
1291        if(row < 0 || row >= this.ds.totalLength){
1292            return;
1293        }
1294
1295        col = (col !== undefined ? col : 0);
1296
1297        var rowInd = row-this.rowIndex;
1298
1299        if (this.rowClipped && row == this.rowIndex+this.visibleRows-1) {
1300            this.adjustScrollerPos(this.rowHeight );
1301        } else if (row >= this.rowIndex+this.visibleRows) {
1302            this.adjustScrollerPos(((row-(this.rowIndex+this.visibleRows))+1)*this.rowHeight);
1303        } else if (row <= this.rowIndex) {
1304            this.adjustScrollerPos((rowInd)*this.rowHeight);
1305        }
1306
1307        var rowEl = this.getRow(row), cellEl;
1308
1309        if(!rowEl){
1310            return;
1311        }
1312
1313        if(!(hscroll === false && col === 0)){
1314            while(this.cm.isHidden(col)){
1315                col++;
1316            }
1317            cellEl = this.getCell(row, col);
1318        }
1319
1320        var c = this.scroller.dom;
1321
1322        if(hscroll !== false){
1323            var cleft = parseInt(cellEl.offsetLeft, 10);
1324            var cright = cleft + cellEl.offsetWidth;
1325
1326            var sleft = parseInt(c.scrollLeft, 10);
1327            var sright = sleft + c.clientWidth;
1328            if(cleft < sleft){
1329                c.scrollLeft = cleft;
1330            }else if(cright > sright){
1331                c.scrollLeft = cright-c.clientWidth;
1332            }
1333        }
1334
1335
1336        return cellEl ?
1337            Ext.fly(cellEl).getXY() :
1338            [c.scrollLeft+this.el.getX(), Ext.fly(rowEl).getY()];
1339    },
1340
1341    /**
1342     * Return strue if the passed record is in the visible rect of this view.
1343     *
1344     * @param {Ext.data.Record} record
1345     *
1346     * @return {Boolean} true if the record is rendered in the view, otherwise false.
1347     */
1348    isRecordRendered : function(record)
1349    {
1350        var ind = this.ds.indexOf(record);
1351
1352        if (ind >= this.rowIndex && ind < this.rowIndex+this.visibleRows) {
1353            return true;
1354        }
1355
1356        return false;
1357    },
1358
1359    /**
1360     * Checks if the passed argument <tt>cursor</tt> lays within a renderable
1361     * area. The area is renderable, if the sum of cursor and the visibleRows
1362     * property does not exceed the current upper buffer limit.
1363     *
1364     * If this method returns <tt>true</tt>, it's basically save to re-render
1365     * the view with <tt>cursor</tt> as the absolute position in the model
1366     * as the first visible row.
1367     *
1368     * @param {Number} cursor The absolute position of the row in the data model.
1369     *
1370     * @return {Boolean} <tt>true</tt>, if the row can be rendered, otherwise
1371     *                   <tt>false</tt>
1372     *
1373     */
1374    isInRange : function(rowIndex)
1375    {
1376        var lastRowIndex = Math.min(this.ds.totalLength-1,
1377                                    rowIndex + (this.visibleRows-1));
1378
1379        return (rowIndex     >= this.ds.bufferRange[0]) &&
1380               (lastRowIndex <= this.ds.bufferRange[1]);
1381    },
1382
1383    /**
1384     * Calculates the bufferRange start index for a buffer request
1385     *
1386     * @param {Boolean} inRange If the index is within the current buffer range
1387     * @param {Number} index The index to use as a reference for the calculations
1388     * @param {Boolean} down Wether the calculation was requested when the user scrolls down
1389     */
1390    getPredictedBufferIndex : function(index, inRange, down)
1391    {
1392        if (!inRange) {
1393            if (index + this.ds.bufferSize >= this.ds.totalLength) {
1394                return this.ds.totalLength - this.ds.bufferSize;
1395            }
1396            // we need at last to render the index + the visible Rows
1397            return Math.max(0, (index + this.visibleRows) - Math.round(this.ds.bufferSize/2));
1398        }
1399        if (!down) {
1400            return Math.max(0, (index-this.ds.bufferSize)+this.visibleRows);
1401        }
1402
1403        if (down) {
1404            return Math.max(0, Math.min(index, this.ds.totalLength-this.ds.bufferSize));
1405        }
1406    },
1407
1408
1409    /**
1410     * Updates the table view. Removes/appends rows as needed and fetches the
1411     * cells content out of the available store. If the needed rows are not within
1412     * the buffer, the method will advise the store to update it's contents.
1413     *
1414     * The method puts the requested cursor into the queue if a previously called
1415     * buffering is in process.
1416     *
1417     * @param {Number} cursor The row's position, absolute to it's position in the
1418     *                        data model
1419     *
1420     */
1421    updateLiveRows: function(index, forceRepaint, forceReload)
1422    {
1423        var inRange = this.isInRange(index);
1424
1425        if (this.isBuffering) {
1426            if (this.isPrebuffering) {
1427                if (inRange) {
1428                    this.replaceLiveRows(index, forceRepaint);
1429                } else {
1430                    this.showLoadMask(true);
1431                }
1432            }
1433
1434            this.fireEvent('cursormove', this, index,
1435                           Math.min(this.ds.totalLength,
1436                           this.visibleRows-this.rowClipped),
1437                           this.ds.totalLength);
1438
1439            this.requestQueue = index;
1440            return;
1441        }
1442
1443        var lastIndex  = this.lastIndex;
1444        this.lastIndex = index;
1445        var inRange    = this.isInRange(index);
1446
1447        var down = false;
1448
1449        if (inRange && forceReload !== true) {
1450
1451            // repaint the table's view
1452            this.replaceLiveRows(index, forceRepaint);
1453            // has to be called AFTER the rowIndex was recalculated
1454            this.fireEvent('cursormove', this, index,
1455                       Math.min(this.ds.totalLength,
1456                       this.visibleRows-this.rowClipped),
1457                       this.ds.totalLength);
1458            // lets decide if we can void this method or stay in here for
1459            // requesting a buffer update
1460            if (index > lastIndex) { // scrolling down
1461
1462                down = true;
1463                var totalCount = this.ds.totalLength;
1464
1465                // while scrolling, we have not yet reached the row index
1466                // that would trigger a re-buffer
1467                if (index+this.visibleRows+this.nearLimit <= this.ds.bufferRange[1]) {
1468                    return;
1469                }
1470
1471                // If we have already buffered the last range we can ever get
1472                // by the queried data repository, we don't need to buffer again.
1473                // This basically means that a re-buffer would only occur again
1474                // if we are scrolling up.
1475                if (this.ds.bufferRange[1]+1 >= totalCount) {
1476                    return;
1477                }
1478            } else if (index < lastIndex) { // scrolling up
1479
1480                down = false;
1481                // We are scrolling up in the first buffer range we can ever get
1482                // Re-buffering would only occur upon scrolling down.
1483                if (this.ds.bufferRange[0] <= 0) {
1484                    return;
1485                }
1486
1487                // if we are scrolling up and we are moving in an acceptable
1488                // buffer range, lets return.
1489                if (index - this.nearLimit > this.ds.bufferRange[0]) {
1490                    return;
1491                }
1492            } else {
1493                return;
1494            }
1495
1496            this.isPrebuffering = true;
1497        }
1498
1499        // prepare for rebuffering
1500        this.isBuffering = true;
1501
1502        var bufferOffset = this.getPredictedBufferIndex(index, inRange, down);
1503
1504        if (!inRange) {
1505            this.showLoadMask(true);
1506        }
1507
1508        this.ds.suspendEvents();
1509        var sInfo  = this.ds.sortInfo;
1510
1511        var params = {};
1512        if (this.ds.lastOptions) {
1513            Ext.apply(params, this.ds.lastOptions.params);
1514        }
1515
1516        params.start = bufferOffset;
1517        params.limit = this.ds.bufferSize;
1518
1519        if (sInfo) {
1520            params.dir  = sInfo.direction;
1521            params.sort = sInfo.field;
1522        }
1523
1524        var opts = {
1525            forceRepaint     : forceRepaint,
1526            callback         : this.liveBufferUpdate,
1527            scope            : this,
1528            params           : params,
1529            suspendLoadEvent : true
1530        };
1531
1532        this.fireEvent('beforebuffer', this, this.ds, index,
1533            Math.min(this.ds.totalLength, this.visibleRows-this.rowClipped),
1534            this.ds.totalLength, opts
1535        );
1536
1537        this.ds.load(opts);
1538        this.ds.resumeEvents();
1539    },
1540
1541    /**
1542     * Shows this' view own load mask to indicate that a large amount of buffer
1543     * data was requested by the store.
1544     * @param {Boolean} show <tt>true</tt> to show the load mask, otherwise
1545     *                       <tt>false</tt>
1546     */
1547    showLoadMask : function(show)
1548    {
1549        if (!this.loadMask || show == this.loadMaskDisplayed) {
1550            return;
1551        }
1552
1553        var dom  = this._loadMaskAnchor.dom;
1554        var data = Ext.Element.data;
1555
1556        var mask    = data(dom, 'mask');
1557        var maskMsg = data(dom, 'maskMsg');
1558
1559        if (show) {
1560            mask.setDisplayed(true);
1561            maskMsg.setDisplayed(true);
1562            maskMsg.center(this._loadMaskAnchor);
1563            // this lines will help IE8 to re-calculate the height of the loadmask
1564            if(Ext.isIE && !(Ext.isIE7 && Ext.isStrict) && this._loadMaskAnchor.getStyle('height') == 'auto'){
1565                    mask.setSize(undefined, this._loadMaskAnchor.getHeight());
1566                }
1567        } else {
1568            mask.setDisplayed(false);
1569            maskMsg.setDisplayed(false);
1570        }
1571
1572        this.loadMaskDisplayed = show;
1573    },
1574
1575    /**
1576     * Renders the table body with the contents of the model. The method will
1577     * prepend/ append rows after removing from either the end or the beginning
1578     * of the table DOM to reduce expensive DOM calls.
1579     * It will also take care of rendering the rows selected, taking the property
1580     * <tt>bufferedSelections</tt> of the {@link BufferedRowSelectionModel} into
1581     * account.
1582     * Instead of calling this method directly, the <tt>updateLiveRows</tt> method
1583     * should be called which takes care of rebuffering if needed, since this method
1584     * will behave erroneous if data of the buffer is requested which may not be
1585     * available.
1586     *
1587     * @param {Number} cursor The position of the data in the model to start
1588     *                        rendering.
1589     *
1590     * @param {Boolean} forceReplace <tt>true</tt> for recomputing the DOM in the
1591     *                               view, otherwise <tt>false</tt>.
1592     */
1593    // private
1594    replaceLiveRows : function(cursor, forceReplace, processRows)
1595    {
1596        var spill = cursor-this.lastRowIndex;
1597
1598        if (spill == 0 && forceReplace !== true) {
1599            return;
1600        }
1601
1602        // decide wether to prepend or append rows
1603        // if spill is negative, we are scrolling up. Thus we have to prepend
1604        // rows. If spill is positive, we have to append the buffers data.
1605        var append = spill > 0;
1606
1607        // abs spill for simplyfiying append/prepend calculations
1608        spill = Math.abs(spill);
1609
1610        // adjust cursor to the buffered model index
1611        var bufferRange = this.ds.bufferRange;
1612        var cursorBuffer = cursor-bufferRange[0];
1613
1614        // compute the last possible renderindex
1615        var lpIndex = Math.min(cursorBuffer+this.visibleRows-1, bufferRange[1]-bufferRange[0]);
1616        // we can skip checking for append or prepend if the spill is larger than
1617        // visibleRows. We can paint the whole rows new then-
1618        if (spill >= this.visibleRows || spill == 0) {
1619            this.mainBody.update(this.renderRows(cursorBuffer, lpIndex));
1620        } else {
1621            if (append) {
1622
1623                this.removeRows(0, spill-1);
1624
1625                if (cursorBuffer+this.visibleRows-spill <= bufferRange[1]-bufferRange[0]) {
1626                    var html = this.renderRows(
1627                        cursorBuffer+this.visibleRows-spill,
1628                        lpIndex
1629                    );
1630                    Ext.DomHelper.insertHtml('beforeEnd', this.mainBody.dom, html);
1631
1632                }
1633
1634            } else {
1635                this.removeRows(this.visibleRows-spill, this.visibleRows-1);
1636                var html = this.renderRows(cursorBuffer, cursorBuffer+spill-1);
1637                Ext.DomHelper.insertHtml('beforeBegin', this.mainBody.dom.firstChild, html);
1638
1639            }
1640        }
1641
1642        if (processRows !== false) {
1643            this.processRows(0, undefined, true);
1644        }
1645        this.lastRowIndex = cursor;
1646    },
1647
1648
1649
1650    /**
1651    * Adjusts the scroller height to make sure each row in the dataset will be
1652    * can be displayed, no matter which value the current height of the grid
1653    * component equals to.
1654    */
1655    // protected
1656    adjustBufferInset : function()
1657    {
1658        var liveScrollerDom = this.liveScroller.dom;
1659        var g = this.grid, ds = g.store;
1660        var c  = g.getGridEl();
1661        var elWidth = c.getSize().width;
1662
1663        // hidden rows is the number of rows which cannot be
1664        // displayed and for which a scrollbar needs to be
1665        // rendered. This does also take clipped rows into account
1666        var hiddenRows = (ds.totalLength == this.visibleRows-this.rowClipped)
1667                       ? 0
1668                       : Math.max(0, ds.totalLength-(this.visibleRows-this.rowClipped));
1669
1670        if (hiddenRows == 0) {
1671            this.scroller.setWidth(elWidth);
1672            liveScrollerDom.style.display = 'none';
1673            return;
1674        } else {
1675            this.scroller.setWidth(elWidth-this.getScrollOffset());
1676            liveScrollerDom.style.display = '';
1677        }
1678
1679        var scrollbar = this.cm.getTotalWidth()+this.getScrollOffset() > elWidth;
1680
1681        // adjust the height of the scrollbar
1682        var contHeight = liveScrollerDom.parentNode.offsetHeight +
1683                         ((ds.totalLength > 0 && scrollbar)
1684                         ? - this.horizontalScrollOffset
1685                         : 0)
1686                         - this.hdHeight;
1687
1688        liveScrollerDom.style.height = Math.max(contHeight, this.horizontalScrollOffset*2)+"px";
1689
1690        if (this.rowHeight == -1) {
1691            return;
1692        }
1693
1694        var h  = (hiddenRows == 0 ? 0 : contHeight+(hiddenRows*this.rowHeight));
1695        var oh = h;
1696        var len = this.liveScrollerInsets.length;
1697
1698        if (h == 0) {
1699            h = 0;
1700        } else {
1701            h = Math.round(h/len);
1702        }
1703
1704        for (var i = 0; i < len; i++) {
1705            if (i == len-1 && h != 0) {
1706                h -= (h*3)-oh;
1707            }
1708            this.liveScrollerInsets[i].style.height = h+"px";
1709        }
1710    },
1711
1712    /**
1713     * Recomputes the number of visible rows in the table based upon the height
1714     * of the component. The method adjusts the <tt>rowIndex</tt> property as
1715     * needed, if the sum of visible rows and the current row index exceeds the
1716     * number of total data available.
1717     */
1718    // protected
1719    adjustVisibleRows : function()
1720    {
1721        if (this.rowHeight == -1) {
1722            if (this.getRows()[0]) {
1723                this.rowHeight = this.getRows()[0].offsetHeight;
1724
1725                if (this.rowHeight <= 0) {
1726                    this.rowHeight = -1;
1727                    return;
1728                }
1729
1730            } else {
1731                return;
1732            }
1733        }
1734
1735
1736        var g = this.grid, ds = g.store;
1737
1738        var c     = g.getGridEl();
1739        var cm    = this.cm;
1740        var size  = c.getSize();
1741        var width = size.width;
1742        var vh    = size.height;
1743
1744        var vw = width-this.getScrollOffset();
1745        // horizontal scrollbar shown?
1746        if (cm.getTotalWidth() > vw) {
1747            // yes!
1748            vh -= this.horizontalScrollOffset;
1749        }
1750
1751        vh -= this.mainHd.getHeight();
1752
1753        var totalLength = ds.totalLength || 0;
1754
1755        var visibleRows = Math.max(1, Math.floor(vh/this.rowHeight));
1756
1757        this.rowClipped = 0;
1758        // only compute the clipped row if the total length of records
1759        // exceeds the number of visible rows displayable
1760        if (totalLength > visibleRows && this.rowHeight / 3 < (vh - (visibleRows*this.rowHeight))) {
1761            visibleRows = Math.min(visibleRows+1, totalLength);
1762            this.rowClipped = 1;
1763        }
1764
1765        // if visibleRows   didn't change, simply void and return.
1766        if (this.visibleRows == visibleRows) {
1767            return;
1768        }
1769
1770        this.visibleRows = visibleRows;
1771
1772        // skip recalculating the row index if we are currently buffering, but not if we
1773        // are just pre-buffering
1774        if (this.isBuffering && !this.isPrebuffering) {
1775            return;
1776        }
1777
1778        // when re-rendering, doe not take the clipped row into account
1779        if (this.rowIndex + (visibleRows-this.rowClipped) > totalLength) {
1780            this.rowIndex     = Math.max(0, totalLength-(visibleRows-this.rowClipped));
1781            this.lastRowIndex = this.rowIndex;
1782        }
1783
1784        this.updateLiveRows(this.rowIndex, true);
1785    },
1786
1787
1788    adjustScrollerPos : function(pixels, suspendEvent)
1789    {
1790        if (pixels == 0) {
1791            return;
1792        }
1793        var liveScroller = this.liveScroller;
1794        var scrollDom    = liveScroller.dom;
1795
1796        if (suspendEvent === true) {
1797            liveScroller.un('scroll', this.onLiveScroll, this);
1798        }
1799        this.lastScrollPos   = scrollDom.scrollTop;
1800        scrollDom.scrollTop += pixels;
1801
1802        if (suspendEvent === true) {
1803            scrollDom.scrollTop = scrollDom.scrollTop;
1804            liveScroller.on('scroll', this.onLiveScroll, this, {buffer : this.scrollDelay});
1805        }
1806
1807    }
1808
1809
1810
1811});
Note: See TracBrowser for help on using the browser.