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