/** * Adds a Load More button at the bottom of the list. When the user presses this button, * the next page of data will be loaded into the store and appended to the List. * * By specifying `{@link #autoPaging}: true`, an 'infinite scroll' effect can be achieved, * i.e., the next page of content will load automatically when the user scrolls to the * bottom of the list. * * ## Example * * Ext.create('Ext.dataview.List', { * * store: Ext.create('TweetStore'), * * plugins: [ * { * xclass: 'Ext.plugin.ListPaging', * autoPaging: true * } * ], * * itemTpl: [ * '<img src="{profile_image_url}" />', * '<div class="tweet">{text}</div>' * ] * }); */Ext.define('Ext.plugin.ListPaging', { extend: 'Ext.Component', alias: 'plugin.listpaging', config: { /** * @cfg {Boolean} autoPaging * True to automatically load the next page when you scroll to the bottom of the list. */ autoPaging: false, /** * @cfg {String} loadMoreText The text used as the label of the Load More button. */ loadMoreText: 'Load More...', /** * @cfg {String} noMoreRecordsText The text used as the label of the Load More button when the Store's * {@link Ext.data.Store#totalCount totalCount} indicates that all of the records available on the server are * already loaded */ noMoreRecordsText: 'No More Records', /** * @private * @cfg {String} loadTpl The template used to render the load more text */ loadTpl: [ '<div class="{cssPrefix}loading-spinner" style="font-size: 180%; margin: 10px auto;">', '<span class="{cssPrefix}loading-top"></span>', '<span class="{cssPrefix}loading-right"></span>', '<span class="{cssPrefix}loading-bottom"></span>', '<span class="{cssPrefix}loading-left"></span>', '</div>', '<div class="{cssPrefix}list-paging-msg">{message}</div>' ].join(''), /** * @cfg {Object} loadMoreCmp * @private */ loadMoreCmp: { xtype: 'component', baseCls: Ext.baseCSSPrefix + 'list-paging', scrollDock: 'bottom', hidden: true }, /** * @private * @cfg {Boolean} loadMoreCmpAdded Indicates whether or not the load more component has been added to the List * yet. */ loadMoreCmpAdded: false, /** * @private * @cfg {String} loadingCls The CSS class that is added to the {@link #loadMoreCmp} while the Store is loading */ loadingCls: Ext.baseCSSPrefix + 'loading', /** * @private * @cfg {Ext.List} list Local reference to the List this plugin is bound to */ list: null, /** * @private * @cfg {Ext.scroll.Scroller} scroller Local reference to the List's Scroller */ scroller: null, /** * @private * @cfg {Boolean} loading True if the plugin has initiated a Store load that has not yet completed */ loading: false }, /** * @private * Sets up all of the references the plugin needs */ init: function(list) { var scroller = list.getScrollable(), store = list.getStore(); this.setList(list); this.setScroller(scroller); this.bindStore(list.getStore()); this.addLoadMoreCmp(); // The List's Store could change at any time so make sure we are informed when that happens list.updateStore = Ext.Function.createInterceptor(list.updateStore, this.bindStore, this); if (this.getAutoPaging()) { scroller.on({ scrollend: this.onScrollEnd, scope: this }); } }, /** * @private */ bindStore: function(newStore, oldStore) { if (oldStore) { oldStore.un({ beforeload: this.onStoreBeforeLoad, load: this.onStoreLoad, filter: this.onFilter, scope: this }); } if (newStore) { newStore.on({ beforeload: this.onStoreBeforeLoad, load: this.onStoreLoad, filter: this.onFilter, scope: this }); } }, /** * @private * Removes the List/DataView's loading mask because we show our own in the plugin. The logic here disables the * loading mask immediately if the store is autoloading. If it's not autoloading, allow the mask to show the first * time the Store loads, then disable it and use the plugin's loading spinner. * @param {Ext.data.Store} store The store that is bound to the DataView */ disableDataViewMask: function() { var list = this.getList(); this._listMask = list.getLoadingText(); list.setLoadingText(null); }, enableDataViewMask: function() { if(this._listMask) { var list = this.getList(); list.setLoadingText(this._listMask); delete this._listMask; } }, /** * @private */ applyLoadTpl: function(config) { return (Ext.isObject(config) && config.isTemplate) ? config : new Ext.XTemplate(config); }, /** * @private */ applyLoadMoreCmp: function(config) { config = Ext.merge(config, { html: this.getLoadTpl().apply({ cssPrefix: Ext.baseCSSPrefix, message: this.getLoadMoreText() }), scrollDock: 'bottom', listeners: { tap: { fn: this.loadNextPage, scope: this, element: 'element' } } }); return Ext.factory(config, Ext.Component, this.getLoadMoreCmp()); }, /** * @private * If we're using autoPaging and detect that the user has scrolled to the bottom, kick off loading of the next page */ onScrollEnd: function(scroller, x, y) { var list = this.getList(); if (!this.getLoading() && y >= scroller.getMaxUserPosition().y) { this.currentScrollToTopOnRefresh = list.getScrollToTopOnRefresh(); list.setScrollToTopOnRefresh(false); this.loadNextPage(); } }, /** * @private * Makes sure we add/remove the loading CSS class while the Store is loading */ updateLoading: function(isLoading) { var loadMoreCmp = this.getLoadMoreCmp(), loadMoreCls = this.getLoadingCls(); if (isLoading) { loadMoreCmp.addCls(loadMoreCls); } else { loadMoreCmp.removeCls(loadMoreCls); } }, /** * @private * If the Store is just about to load but it's currently empty, we hide the load more button because this is * usually an outcome of setting a new Store on the List so we don't want the load more button to flash while * the new Store loads */ onStoreBeforeLoad: function(store) { if (store.getCount() === 0) { this.getLoadMoreCmp().hide(); } }, /** * @private */ onStoreLoad: function(store) { var loadCmp = this.getLoadMoreCmp(), template = this.getLoadTpl(), message = this.storeFullyLoaded() ? this.getNoMoreRecordsText() : this.getLoadMoreText(); if (store.getCount()) { loadCmp.show(); } this.setLoading(false); //if we've reached the end of the data set, switch to the noMoreRecordsText loadCmp.setHtml(template.apply({ cssPrefix: Ext.baseCSSPrefix, message: message })); if (this.currentScrollToTopOnRefresh !== undefined) { this.getList().setScrollToTopOnRefresh(this.currentScrollToTopOnRefresh); delete this.currentScrollToTopOnRefresh; } this.enableDataViewMask(); }, onFilter: function(store) { if (store.getCount() === 0) { this.getLoadMoreCmp().hide(); }else { this.getLoadMoreCmp().show(); } }, /** * @private * Because the attached List's inner list element is rendered after our init function is called, * we need to dynamically add the loadMoreCmp later. This does this once and caches the result. */ addLoadMoreCmp: function() { var list = this.getList(), cmp = this.getLoadMoreCmp(); if (!this.getLoadMoreCmpAdded()) { list.add(cmp); /** * @event loadmorecmpadded Fired when the Load More component is added to the list. Fires on the List. * @param {Ext.plugin.ListPaging} this The list paging plugin * @param {Ext.List} list The list */ list.fireEvent('loadmorecmpadded', this, list); this.setLoadMoreCmpAdded(true); } return cmp; }, /** * @private * Returns true if the Store is detected as being fully loaded, or the server did not return a total count, which * means we're in 'infinite' mode * @return {Boolean} */ storeFullyLoaded: function() { var store = this.getList().getStore(), total = store.getTotalCount(); return total !== null ? store.getTotalCount() <= (store.currentPage * store.getPageSize()) : false; }, /** * @private */ loadNextPage: function() { var me = this; if (!me.storeFullyLoaded()) { me.disableDataViewMask(); me.setLoading(true); me.getList().getStore().nextPage({ addRecords: true }); } }});