Upgrade to ExtJS 4.0.2 - Released 06/09/2011
[extjs.git] / src / toolbar / Paging.js
1 /*
2
3 This file is part of Ext JS 4
4
5 Copyright (c) 2011 Sencha Inc
6
7 Contact:  http://www.sencha.com/contact
8
9 GNU General Public License Usage
10 This file may be used under the terms of the GNU General Public License version 3.0 as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file.  Please review the following information to ensure the GNU General Public License version 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html.
11
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
13
14 */
15 /**
16  * @class Ext.toolbar.Paging
17  * @extends Ext.toolbar.Toolbar
18  * <p>As the amount of records increases, the time required for the browser to render
19  * them increases. Paging is used to reduce the amount of data exchanged with the client.
20  * Note: if there are more records/rows than can be viewed in the available screen area, vertical
21  * scrollbars will be added.</p>
22  * <p>Paging is typically handled on the server side (see exception below). The client sends
23  * parameters to the server side, which the server needs to interpret and then respond with the
24  * appropriate data.</p>
25  * <p><b>Ext.toolbar.Paging</b> is a specialized toolbar that is bound to a {@link Ext.data.Store}
26  * and provides automatic paging control. This Component {@link Ext.data.Store#load load}s blocks
27  * of data into the <tt>{@link #store}</tt> by passing {@link Ext.data.Store#paramNames paramNames} used for
28  * paging criteria.</p>
29  *
30  * {@img Ext.toolbar.Paging/Ext.toolbar.Paging.png Ext.toolbar.Paging component}
31  *
32  * <p>PagingToolbar is typically used as one of the Grid's toolbars:</p>
33  * <pre><code>
34  *    var itemsPerPage = 2;   // set the number of items you want per page
35  *    
36  *    var store = Ext.create('Ext.data.Store', {
37  *        id:'simpsonsStore',
38  *        autoLoad: false,
39  *        fields:['name', 'email', 'phone'],
40  *        pageSize: itemsPerPage, // items per page
41  *        proxy: {
42  *            type: 'ajax',
43  *            url: 'pagingstore.js',  // url that will load data with respect to start and limit params
44  *            reader: {
45  *                type: 'json',
46  *                root: 'items',
47  *                totalProperty: 'total'
48  *            }
49  *        }
50  *    });
51  *    
52  *    // specify segment of data you want to load using params
53  *    store.load({
54  *        params:{
55  *            start:0,    
56  *            limit: itemsPerPage
57  *        }
58  *    });
59  *    
60  *    Ext.create('Ext.grid.Panel', {
61  *        title: 'Simpsons',
62  *        store: store,
63  *        columns: [
64  *            {header: 'Name',  dataIndex: 'name'},
65  *            {header: 'Email', dataIndex: 'email', flex:1},
66  *            {header: 'Phone', dataIndex: 'phone'}
67  *        ],
68  *        width: 400,
69  *        height: 125,
70  *        dockedItems: [{
71  *            xtype: 'pagingtoolbar',
72  *            store: store,   // same store GridPanel is using
73  *            dock: 'bottom',
74  *            displayInfo: true
75  *        }],
76  *        renderTo: Ext.getBody()
77  *    });
78  * </code></pre>
79  *
80  * <p>To use paging, pass the paging requirements to the server when the store is first loaded.</p>
81  * <pre><code>
82 store.load({
83     params: {
84         // specify params for the first page load if using paging
85         start: 0,          
86         limit: myPageSize,
87         // other params
88         foo:   'bar'
89     }
90 });
91  * </code></pre>
92  * 
93  * <p>If using {@link Ext.data.Store#autoLoad store's autoLoad} configuration:</p>
94  * <pre><code>
95 var myStore = new Ext.data.Store({
96     {@link Ext.data.Store#autoLoad autoLoad}: {start: 0, limit: 25},
97     ...
98 });
99  * </code></pre>
100  * 
101  * <p>The packet sent back from the server would have this form:</p>
102  * <pre><code>
103 {
104     "success": true,
105     "results": 2000, 
106     "rows": [ // <b>*Note:</b> this must be an Array 
107         { "id":  1, "name": "Bill", "occupation": "Gardener" },
108         { "id":  2, "name":  "Ben", "occupation": "Horticulturalist" },
109         ...
110         { "id": 25, "name":  "Sue", "occupation": "Botanist" }
111     ]
112 }
113  * </code></pre>
114  * <p><u>Paging with Local Data</u></p>
115  * <p>Paging can also be accomplished with local data using extensions:</p>
116  * <div class="mdetail-params"><ul>
117  * <li><a href="http://sencha.com/forum/showthread.php?t=71532">Ext.ux.data.PagingStore</a></li>
118  * <li>Paging Memory Proxy (examples/ux/PagingMemoryProxy.js)</li>
119  * </ul></div>
120  */
121 Ext.define('Ext.toolbar.Paging', {
122     extend: 'Ext.toolbar.Toolbar',
123     alias: 'widget.pagingtoolbar',
124     alternateClassName: 'Ext.PagingToolbar',
125     requires: ['Ext.toolbar.TextItem', 'Ext.form.field.Number'],
126     /**
127      * @cfg {Ext.data.Store} store
128      * The {@link Ext.data.Store} the paging toolbar should use as its data source (required).
129      */
130     /**
131      * @cfg {Boolean} displayInfo
132      * <tt>true</tt> to display the displayMsg (defaults to <tt>false</tt>)
133      */
134     displayInfo: false,
135     /**
136      * @cfg {Boolean} prependButtons
137      * <tt>true</tt> to insert any configured <tt>items</tt> <i>before</i> the paging buttons.
138      * Defaults to <tt>false</tt>.
139      */
140     prependButtons: false,
141     /**
142      * @cfg {String} displayMsg
143      * The paging status message to display (defaults to <tt>'Displaying {0} - {1} of {2}'</tt>).
144      * Note that this string is formatted using the braced numbers <tt>{0}-{2}</tt> as tokens
145      * that are replaced by the values for start, end and total respectively. These tokens should
146      * be preserved when overriding this string if showing those values is desired.
147      */
148     displayMsg : 'Displaying {0} - {1} of {2}',
149     /**
150      * @cfg {String} emptyMsg
151      * The message to display when no records are found (defaults to 'No data to display')
152      */
153     emptyMsg : 'No data to display',
154     /**
155      * @cfg {String} beforePageText
156      * The text displayed before the input item (defaults to <tt>'Page'</tt>).
157      */
158     beforePageText : 'Page',
159     /**
160      * @cfg {String} afterPageText
161      * Customizable piece of the default paging text (defaults to <tt>'of {0}'</tt>). Note that
162      * this string is formatted using <tt>{0}</tt> as a token that is replaced by the number of
163      * total pages. This token should be preserved when overriding this string if showing the
164      * total page count is desired.
165      */
166     afterPageText : 'of {0}',
167     /**
168      * @cfg {String} firstText
169      * The quicktip text displayed for the first page button (defaults to <tt>'First Page'</tt>).
170      * <b>Note</b>: quick tips must be initialized for the quicktip to show.
171      */
172     firstText : 'First Page',
173     /**
174      * @cfg {String} prevText
175      * The quicktip text displayed for the previous page button (defaults to <tt>'Previous Page'</tt>).
176      * <b>Note</b>: quick tips must be initialized for the quicktip to show.
177      */
178     prevText : 'Previous Page',
179     /**
180      * @cfg {String} nextText
181      * The quicktip text displayed for the next page button (defaults to <tt>'Next Page'</tt>).
182      * <b>Note</b>: quick tips must be initialized for the quicktip to show.
183      */
184     nextText : 'Next Page',
185     /**
186      * @cfg {String} lastText
187      * The quicktip text displayed for the last page button (defaults to <tt>'Last Page'</tt>).
188      * <b>Note</b>: quick tips must be initialized for the quicktip to show.
189      */
190     lastText : 'Last Page',
191     /**
192      * @cfg {String} refreshText
193      * The quicktip text displayed for the Refresh button (defaults to <tt>'Refresh'</tt>).
194      * <b>Note</b>: quick tips must be initialized for the quicktip to show.
195      */
196     refreshText : 'Refresh',
197     /**
198      * @cfg {Number} inputItemWidth
199      * The width in pixels of the input field used to display and change the current page number (defaults to 30).
200      */
201     inputItemWidth : 30,
202     
203     /**
204      * Gets the standard paging items in the toolbar
205      * @private
206      */
207     getPagingItems: function() {
208         var me = this;
209         
210         return [{
211             itemId: 'first',
212             tooltip: me.firstText,
213             overflowText: me.firstText,
214             iconCls: Ext.baseCSSPrefix + 'tbar-page-first',
215             disabled: true,
216             handler: me.moveFirst,
217             scope: me
218         },{
219             itemId: 'prev',
220             tooltip: me.prevText,
221             overflowText: me.prevText,
222             iconCls: Ext.baseCSSPrefix + 'tbar-page-prev',
223             disabled: true,
224             handler: me.movePrevious,
225             scope: me
226         },
227         '-',
228         me.beforePageText,
229         {
230             xtype: 'numberfield',
231             itemId: 'inputItem',
232             name: 'inputItem',
233             cls: Ext.baseCSSPrefix + 'tbar-page-number',
234             allowDecimals: false,
235             minValue: 1,
236             hideTrigger: true,
237             enableKeyEvents: true,
238             selectOnFocus: true,
239             submitValue: false,
240             width: me.inputItemWidth,
241             margins: '-1 2 3 2',
242             listeners: {
243                 scope: me,
244                 keydown: me.onPagingKeyDown,
245                 blur: me.onPagingBlur
246             }
247         },{
248             xtype: 'tbtext',
249             itemId: 'afterTextItem',
250             text: Ext.String.format(me.afterPageText, 1)
251         },
252         '-',
253         {
254             itemId: 'next',
255             tooltip: me.nextText,
256             overflowText: me.nextText,
257             iconCls: Ext.baseCSSPrefix + 'tbar-page-next',
258             disabled: true,
259             handler: me.moveNext,
260             scope: me
261         },{
262             itemId: 'last',
263             tooltip: me.lastText,
264             overflowText: me.lastText,
265             iconCls: Ext.baseCSSPrefix + 'tbar-page-last',
266             disabled: true,
267             handler: me.moveLast,
268             scope: me
269         },
270         '-',
271         {
272             itemId: 'refresh',
273             tooltip: me.refreshText,
274             overflowText: me.refreshText,
275             iconCls: Ext.baseCSSPrefix + 'tbar-loading',
276             handler: me.doRefresh,
277             scope: me
278         }];
279     },
280
281     initComponent : function(){
282         var me = this,
283             pagingItems = me.getPagingItems(),
284             userItems   = me.items || me.buttons || [];
285             
286         if (me.prependButtons) {
287             me.items = userItems.concat(pagingItems);
288         } else {
289             me.items = pagingItems.concat(userItems);
290         }
291         delete me.buttons;
292         
293         if (me.displayInfo) {
294             me.items.push('->');
295             me.items.push({xtype: 'tbtext', itemId: 'displayItem'});
296         }
297         
298         me.callParent();
299         
300         me.addEvents(
301             /**
302              * @event change
303              * Fires after the active page has been changed.
304              * @param {Ext.toolbar.Paging} this
305              * @param {Object} pageData An object that has these properties:<ul>
306              * <li><code>total</code> : Number <div class="sub-desc">The total number of records in the dataset as
307              * returned by the server</div></li>
308              * <li><code>currentPage</code> : Number <div class="sub-desc">The current page number</div></li>
309              * <li><code>pageCount</code> : Number <div class="sub-desc">The total number of pages (calculated from
310              * the total number of records in the dataset as returned by the server and the current {@link #pageSize})</div></li>
311              * <li><code>toRecord</code> : Number <div class="sub-desc">The starting record index for the current page</div></li>
312              * <li><code>fromRecord</code> : Number <div class="sub-desc">The ending record index for the current page</div></li>
313              * </ul>
314              */
315             'change',
316             /**
317              * @event beforechange
318              * Fires just before the active page is changed.
319              * Return false to prevent the active page from being changed.
320              * @param {Ext.toolbar.Paging} this
321              * @param {Number} page The page number that will be loaded on change 
322              */
323             'beforechange'
324         );
325         me.on('afterlayout', me.onLoad, me, {single: true});
326
327         me.bindStore(me.store, true);
328     },
329     // private
330     updateInfo : function(){
331         var me = this,
332             displayItem = me.child('#displayItem'),
333             store = me.store,
334             pageData = me.getPageData(),
335             count, msg;
336
337         if (displayItem) {
338             count = store.getCount();
339             if (count === 0) {
340                 msg = me.emptyMsg;
341             } else {
342                 msg = Ext.String.format(
343                     me.displayMsg,
344                     pageData.fromRecord,
345                     pageData.toRecord,
346                     pageData.total
347                 );
348             }
349             displayItem.setText(msg);
350             me.doComponentLayout();
351         }
352     },
353
354     // private
355     onLoad : function(){
356         var me = this,
357             pageData,
358             currPage,
359             pageCount,
360             afterText;
361             
362         if (!me.rendered) {
363             return;
364         }
365
366         pageData = me.getPageData();
367         currPage = pageData.currentPage;
368         pageCount = pageData.pageCount;
369         afterText = Ext.String.format(me.afterPageText, isNaN(pageCount) ? 1 : pageCount);
370
371         me.child('#afterTextItem').setText(afterText);
372         me.child('#inputItem').setValue(currPage);
373         me.child('#first').setDisabled(currPage === 1);
374         me.child('#prev').setDisabled(currPage === 1);
375         me.child('#next').setDisabled(currPage === pageCount);
376         me.child('#last').setDisabled(currPage === pageCount);
377         me.child('#refresh').enable();
378         me.updateInfo();
379         me.fireEvent('change', me, pageData);
380     },
381
382     // private
383     getPageData : function(){
384         var store = this.store,
385             totalCount = store.getTotalCount();
386             
387         return {
388             total : totalCount,
389             currentPage : store.currentPage,
390             pageCount: Math.ceil(totalCount / store.pageSize),
391             fromRecord: ((store.currentPage - 1) * store.pageSize) + 1,
392             toRecord: Math.min(store.currentPage * store.pageSize, totalCount)
393             
394         };
395     },
396
397     // private
398     onLoadError : function(){
399         if (!this.rendered) {
400             return;
401         }
402         this.child('#refresh').enable();
403     },
404
405     // private
406     readPageFromInput : function(pageData){
407         var v = this.child('#inputItem').getValue(),
408             pageNum = parseInt(v, 10);
409             
410         if (!v || isNaN(pageNum)) {
411             this.child('#inputItem').setValue(pageData.currentPage);
412             return false;
413         }
414         return pageNum;
415     },
416
417     onPagingFocus : function(){
418         this.child('#inputItem').select();
419     },
420
421     //private
422     onPagingBlur : function(e){
423         var curPage = this.getPageData().currentPage;
424         this.child('#inputItem').setValue(curPage);
425     },
426
427     // private
428     onPagingKeyDown : function(field, e){
429         var me = this,
430             k = e.getKey(),
431             pageData = me.getPageData(),
432             increment = e.shiftKey ? 10 : 1,
433             pageNum;
434
435         if (k == e.RETURN) {
436             e.stopEvent();
437             pageNum = me.readPageFromInput(pageData);
438             if (pageNum !== false) {
439                 pageNum = Math.min(Math.max(1, pageNum), pageData.pageCount);
440                 if(me.fireEvent('beforechange', me, pageNum) !== false){
441                     me.store.loadPage(pageNum);
442                 }
443             }
444         } else if (k == e.HOME || k == e.END) {
445             e.stopEvent();
446             pageNum = k == e.HOME ? 1 : pageData.pageCount;
447             field.setValue(pageNum);
448         } else if (k == e.UP || k == e.PAGEUP || k == e.DOWN || k == e.PAGEDOWN) {
449             e.stopEvent();
450             pageNum = me.readPageFromInput(pageData);
451             if (pageNum) {
452                 if (k == e.DOWN || k == e.PAGEDOWN) {
453                     increment *= -1;
454                 }
455                 pageNum += increment;
456                 if (pageNum >= 1 && pageNum <= pageData.pages) {
457                     field.setValue(pageNum);
458                 }
459             }
460         }
461     },
462
463     // private
464     beforeLoad : function(){
465         if(this.rendered && this.refresh){
466             this.refresh.disable();
467         }
468     },
469
470     // private
471     doLoad : function(start){
472         if(this.fireEvent('beforechange', this, o) !== false){
473             this.store.load();
474         }
475     },
476
477     /**
478      * Move to the first page, has the same effect as clicking the 'first' button.
479      */
480     moveFirst : function(){
481         if (this.fireEvent('beforechange', this, 1) !== false){
482             this.store.loadPage(1);
483         }
484     },
485
486     /**
487      * Move to the previous page, has the same effect as clicking the 'previous' button.
488      */
489     movePrevious : function(){
490         var me = this,
491             prev = me.store.currentPage - 1;
492         
493         if (prev > 0) {
494             if (me.fireEvent('beforechange', me, prev) !== false) {
495                 me.store.previousPage();
496             }
497         }
498     },
499
500     /**
501      * Move to the next page, has the same effect as clicking the 'next' button.
502      */
503     moveNext : function(){
504         var me = this,
505             total = me.getPageData().pageCount,
506             next = me.store.currentPage + 1;
507                
508         if (next <= total) {
509             if (me.fireEvent('beforechange', me, next) !== false) {
510                 me.store.nextPage();
511             }
512         }
513     },
514
515     /**
516      * Move to the last page, has the same effect as clicking the 'last' button.
517      */
518     moveLast : function(){
519         var me = this, 
520             last = me.getPageData().pageCount;
521         
522         if (me.fireEvent('beforechange', me, last) !== false) {
523             me.store.loadPage(last);
524         }
525     },
526
527     /**
528      * Refresh the current page, has the same effect as clicking the 'refresh' button.
529      */
530     doRefresh : function(){
531         var me = this,
532             current = me.store.currentPage;
533         
534         if (me.fireEvent('beforechange', me, current) !== false) {
535             me.store.loadPage(current);
536         }
537     },
538
539     /**
540      * Binds the paging toolbar to the specified {@link Ext.data.Store}
541      * @param {Store} store The store to bind to this toolbar
542      * @param {Boolean} initial (Optional) true to not remove listeners
543      */
544     bindStore : function(store, initial){
545         var me = this;
546         
547         if (!initial && me.store) {
548             if(store !== me.store && me.store.autoDestroy){
549                 me.store.destroy();
550             }else{
551                 me.store.un('beforeload', me.beforeLoad, me);
552                 me.store.un('load', me.onLoad, me);
553                 me.store.un('exception', me.onLoadError, me);
554             }
555             if(!store){
556                 me.store = null;
557             }
558         }
559         if (store) {
560             store = Ext.data.StoreManager.lookup(store);
561             store.on({
562                 scope: me,
563                 beforeload: me.beforeLoad,
564                 load: me.onLoad,
565                 exception: me.onLoadError
566             });
567         }
568         me.store = store;
569     },
570
571     /**
572      * Unbinds the paging toolbar from the specified {@link Ext.data.Store} <b>(deprecated)</b>
573      * @param {Ext.data.Store} store The data store to unbind
574      */
575     unbind : function(store){
576         this.bindStore(null);
577     },
578
579     /**
580      * Binds the paging toolbar to the specified {@link Ext.data.Store} <b>(deprecated)</b>
581      * @param {Ext.data.Store} store The data store to bind
582      */
583     bind : function(store){
584         this.bindStore(store);
585     },
586
587     // private
588     onDestroy : function(){
589         this.bindStore(null);
590         this.callParent();
591     }
592 });
593