]> git.proxmox.com Git - sencha-touch.git/blob - src/src/plugin/ListPaging.js
import Sencha Touch 2.4.2 source
[sencha-touch.git] / src / src / plugin / ListPaging.js
1 /**
2 * Adds a Load More button at the bottom of the list. When the user presses this button,
3 * the next page of data will be loaded into the store and appended to the List.
4 *
5 * By specifying `{@link #autoPaging}: true`, an 'infinite scroll' effect can be achieved,
6 * i.e., the next page of content will load automatically when the user scrolls to the
7 * bottom of the list.
8 *
9 * ## Example
10 *
11 * Ext.create('Ext.dataview.List', {
12 *
13 * store: Ext.create('TweetStore'),
14 *
15 * plugins: [
16 * {
17 * xclass: 'Ext.plugin.ListPaging',
18 * autoPaging: true
19 * }
20 * ],
21 *
22 * itemTpl: [
23 * '<img src="{profile_image_url}" />',
24 * '<div class="tweet">{text}</div>'
25 * ]
26 * });
27 */
28 Ext.define('Ext.plugin.ListPaging', {
29 extend: 'Ext.Component',
30 alias: 'plugin.listpaging',
31
32 config: {
33 /**
34 * @cfg {Boolean} autoPaging
35 * True to automatically load the next page when you scroll to the bottom of the list.
36 */
37 autoPaging: false,
38
39 /**
40 * @cfg {String} loadMoreText The text used as the label of the Load More button.
41 */
42 loadMoreText: 'Load More...',
43
44 /**
45 * @cfg {String} noMoreRecordsText The text used as the label of the Load More button when the Store's
46 * {@link Ext.data.Store#totalCount totalCount} indicates that all of the records available on the server are
47 * already loaded
48 */
49 noMoreRecordsText: 'No More Records',
50
51 /**
52 * @private
53 * @cfg {String} loadTpl The template used to render the load more text
54 */
55 loadTpl: [
56 '<div class="{cssPrefix}loading-spinner" style="font-size: 180%; margin: 10px auto;">',
57 '<span class="{cssPrefix}loading-top"></span>',
58 '<span class="{cssPrefix}loading-right"></span>',
59 '<span class="{cssPrefix}loading-bottom"></span>',
60 '<span class="{cssPrefix}loading-left"></span>',
61 '</div>',
62 '<div class="{cssPrefix}list-paging-msg">{message}</div>'
63 ].join(''),
64
65 /**
66 * @cfg {Object} loadMoreCmp
67 * @private
68 */
69 loadMoreCmp: {
70 xtype: 'component',
71 baseCls: Ext.baseCSSPrefix + 'list-paging',
72 scrollDock: 'bottom',
73 hidden: true
74 },
75
76 /**
77 * @private
78 * @cfg {Boolean} loadMoreCmpAdded Indicates whether or not the load more component has been added to the List
79 * yet.
80 */
81 loadMoreCmpAdded: false,
82
83 /**
84 * @private
85 * @cfg {String} loadingCls The CSS class that is added to the {@link #loadMoreCmp} while the Store is loading
86 */
87 loadingCls: Ext.baseCSSPrefix + 'loading',
88
89 /**
90 * @private
91 * @cfg {Ext.List} list Local reference to the List this plugin is bound to
92 */
93 list: null,
94
95 /**
96 * @private
97 * @cfg {Ext.scroll.Scroller} scroller Local reference to the List's Scroller
98 */
99 scroller: null,
100
101 /**
102 * @private
103 * @cfg {Boolean} loading True if the plugin has initiated a Store load that has not yet completed
104 */
105 loading: false
106 },
107
108 /**
109 * @private
110 * Sets up all of the references the plugin needs
111 */
112 init: function(list) {
113 var scroller = list.getScrollable().getScroller(),
114 store = list.getStore();
115
116 this.setList(list);
117 this.setScroller(scroller);
118 this.bindStore(list.getStore());
119
120 this.addLoadMoreCmp();
121
122 // The List's Store could change at any time so make sure we are informed when that happens
123 list.updateStore = Ext.Function.createInterceptor(list.updateStore, this.bindStore, this);
124
125 if (this.getAutoPaging()) {
126 scroller.on({
127 scrollend: this.onScrollEnd,
128 scope: this
129 });
130 }
131 },
132
133 /**
134 * @private
135 */
136 bindStore: function(newStore, oldStore) {
137 if (oldStore) {
138 oldStore.un({
139 beforeload: this.onStoreBeforeLoad,
140 load: this.onStoreLoad,
141 filter: this.onFilter,
142 scope: this
143 });
144 }
145
146 if (newStore) {
147 newStore.on({
148 beforeload: this.onStoreBeforeLoad,
149 load: this.onStoreLoad,
150 filter: this.onFilter,
151 scope: this
152 });
153 }
154 },
155
156 /**
157 * @private
158 * Removes the List/DataView's loading mask because we show our own in the plugin. The logic here disables the
159 * loading mask immediately if the store is autoloading. If it's not autoloading, allow the mask to show the first
160 * time the Store loads, then disable it and use the plugin's loading spinner.
161 * @param {Ext.data.Store} store The store that is bound to the DataView
162 */
163 disableDataViewMask: function() {
164 var list = this.getList();
165 this._listMask = list.getLoadingText();
166
167 list.setLoadingText(null);
168 },
169
170 enableDataViewMask: function() {
171 if(this._listMask) {
172 var list = this.getList();
173 list.setLoadingText(this._listMask);
174 delete this._listMask;
175 }
176 },
177
178 /**
179 * @private
180 */
181 applyLoadTpl: function(config) {
182 return (Ext.isObject(config) && config.isTemplate) ? config : new Ext.XTemplate(config);
183 },
184
185 /**
186 * @private
187 */
188 applyLoadMoreCmp: function(config) {
189 config = Ext.merge(config, {
190 html: this.getLoadTpl().apply({
191 cssPrefix: Ext.baseCSSPrefix,
192 message: this.getLoadMoreText()
193 }),
194 scrollDock: 'bottom',
195 listeners: {
196 tap: {
197 fn: this.loadNextPage,
198 scope: this,
199 element: 'element'
200 }
201 }
202 });
203
204 return Ext.factory(config, Ext.Component, this.getLoadMoreCmp());
205 },
206
207 /**
208 * @private
209 * If we're using autoPaging and detect that the user has scrolled to the bottom, kick off loading of the next page
210 */
211 onScrollEnd: function(scroller, x, y) {
212 var list = this.getList();
213
214 if (!this.getLoading() && y >= scroller.maxPosition.y) {
215 this.currentScrollToTopOnRefresh = list.getScrollToTopOnRefresh();
216 list.setScrollToTopOnRefresh(false);
217
218 this.loadNextPage();
219 }
220 },
221
222 /**
223 * @private
224 * Makes sure we add/remove the loading CSS class while the Store is loading
225 */
226 updateLoading: function(isLoading) {
227 var loadMoreCmp = this.getLoadMoreCmp(),
228 loadMoreCls = this.getLoadingCls();
229
230 if (isLoading) {
231 loadMoreCmp.addCls(loadMoreCls);
232 } else {
233 loadMoreCmp.removeCls(loadMoreCls);
234 }
235 },
236
237 /**
238 * @private
239 * If the Store is just about to load but it's currently empty, we hide the load more button because this is
240 * usually an outcome of setting a new Store on the List so we don't want the load more button to flash while
241 * the new Store loads
242 */
243 onStoreBeforeLoad: function(store) {
244 if (store.getCount() === 0) {
245 this.getLoadMoreCmp().hide();
246 }
247 },
248
249 /**
250 * @private
251 */
252 onStoreLoad: function(store) {
253 var loadCmp = this.getLoadMoreCmp(),
254 template = this.getLoadTpl(),
255 message = this.storeFullyLoaded() ? this.getNoMoreRecordsText() : this.getLoadMoreText();
256
257 if (store.getCount()) {
258 loadCmp.show();
259 }
260 this.setLoading(false);
261
262 //if we've reached the end of the data set, switch to the noMoreRecordsText
263 loadCmp.setHtml(template.apply({
264 cssPrefix: Ext.baseCSSPrefix,
265 message: message
266 }));
267
268 if (this.currentScrollToTopOnRefresh !== undefined) {
269 this.getList().setScrollToTopOnRefresh(this.currentScrollToTopOnRefresh);
270 delete this.currentScrollToTopOnRefresh;
271 }
272
273 this.enableDataViewMask();
274 },
275
276 onFilter: function(store) {
277 if (store.getCount() === 0) {
278 this.getLoadMoreCmp().hide();
279 }else {
280 this.getLoadMoreCmp().show();
281 }
282 },
283
284 /**
285 * @private
286 * Because the attached List's inner list element is rendered after our init function is called,
287 * we need to dynamically add the loadMoreCmp later. This does this once and caches the result.
288 */
289 addLoadMoreCmp: function() {
290 var list = this.getList(),
291 cmp = this.getLoadMoreCmp();
292
293 if (!this.getLoadMoreCmpAdded()) {
294 list.add(cmp);
295
296 /**
297 * @event loadmorecmpadded Fired when the Load More component is added to the list. Fires on the List.
298 * @param {Ext.plugin.ListPaging} this The list paging plugin
299 * @param {Ext.List} list The list
300 */
301 list.fireEvent('loadmorecmpadded', this, list);
302 this.setLoadMoreCmpAdded(true);
303 }
304
305 return cmp;
306 },
307
308 /**
309 * @private
310 * Returns true if the Store is detected as being fully loaded, or the server did not return a total count, which
311 * means we're in 'infinite' mode
312 * @return {Boolean}
313 */
314 storeFullyLoaded: function() {
315 var store = this.getList().getStore(),
316 total = store.getTotalCount();
317
318 return total !== null ? store.getTotalCount() <= (store.currentPage * store.getPageSize()) : false;
319 },
320
321 /**
322 * @private
323 */
324 loadNextPage: function() {
325 var me = this;
326 if (!me.storeFullyLoaded()) {
327 me.disableDataViewMask();
328 me.setLoading(true);
329 me.getList().getStore().nextPage({ addRecords: true });
330 }
331 }
332 });