/** * 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 near the * bottom of the list. * * ## Example * * Ext.create('Ext.dataview.List', { * * store: Ext.create('TweetStore'), * * plugins: { * listpaging: { * autoPaging: true * } * }, * * itemTpl: [ * '<img src="{profile_image_url}" />', * '<div class="tweet">{text}</div>' * ] * }); */Ext.define('Ext.dataview.plugin.ListPaging', { extend: 'Ext.plugin.Abstract', alias: 'plugin.listpaging', alternateClassName: 'Ext.plugin.ListPaging', config: { /** * @cfg {Boolean} autoPaging * True to automatically load the next page as soon as less than {@link #bufferZone} * items are available besides the ones currently visible. */ autoPaging: false, /** * @cfg {Number} bufferZone * Amount of items, besides the ones currently visible, that need to be available until * the next page is loaded. If 0 (or null), the next page is loaded when the list is * scrolled to the bottom. This config only applies if {@link #autoPaging} is true. */ bufferZone: 8, /** * @cfg {String} loadMoreText The text used as the label of the Load More button. * * @locale */ 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 * * @locale */ noMoreRecordsText: 'No More Records', /** * @cfg {Object} loadMoreCmp * @private */ loadMoreCmp: { xtype: 'component', cls: Ext.baseCSSPrefix + 'listpaging', scrollDock: 'end', hidden: true, inheritUi: true }, /** * @private * @cfg {Boolean} loading True if the plugin has initiated a Store load that has * not yet completed. */ loading: false }, loadTpl: '<div class="' + Ext.baseCSSPrefix + 'loading-spinner">' + '<span class="' + Ext.baseCSSPrefix + 'loading-top"></span>' + '<span class="' + Ext.baseCSSPrefix + 'loading-right"></span>' + '<span class="' + Ext.baseCSSPrefix + 'loading-bottom"></span>' + '<span class="' + Ext.baseCSSPrefix + 'loading-left"></span>' + '</div>' + '<div class="' + Ext.baseCSSPrefix + 'message">{message}</div>', /** * @private * Sets up all of the references the plugin needs */ init: function(list) { var me = this; list.on('storechange', 'onStoreChange', me); me.bindStore(list.getStore()); me.addLoadMoreCmp(); }, destroy: function() { Ext.destroy(this._storeListeners); this.callParent(); }, updateAutoPaging: function(enabled) { var scroller = this.getCmp().getScrollable(), listeners = { scroll: 'onScroll', scope: this }; if (enabled) { scroller.on(listeners); this.ensureBufferZone(); } else { scroller.un(listeners); } }, /** * @private */ bindStore: function(store) { var me = this, listeners = { beforeload: 'onStoreBeforeLoad', load: 'onStoreLoad', filter: 'onFilter', destroyable: true, scope: me }; me._storeListeners = Ext.destroy(me._storeListeners); if (store) { me._storeListeners = store.on(listeners); } }, /** * @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.cmp; this._listMask = list.getLoadingText(); list.setLoadingText(null); }, enableDataViewMask: function() { var list; if (this._listMask) { list = this.cmp; list.setLoadingText(this._listMask); delete this._listMask; } }, /** * @private */ applyLoadMoreCmp: function(config, instance) { return Ext.updateWidget(instance, config, this, 'createLoadMoreCmp'); }, createLoadMoreCmp: function(config) { return Ext.apply({ html: this.getLoadTpl().apply({ message: this.getLoadMoreText() }) }, config); }, updateLoadMoreCmp: function(loadMoreCmp, old) { Ext.destroy(old); if (loadMoreCmp) { loadMoreCmp.el.on({ tap: 'loadNextPage', scope: this }); } }, /** * @private * If we're using autoPaging and detect that the user has scrolled to the bottom, * kick off loading of the next page. */ onScroll: function() { this.ensureBufferZone(); }, /** * @private * Makes sure we add/remove the loading CSS class while the Store is loading */ updateLoading: function(isLoading) { this.getLoadMoreCmp().toggleCls(this.loadingCls, isLoading); }, /** * @private */ onStoreChange: function(list, store) { this.bindStore(store); }, /** * @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() { this.syncState(); }, onFilter: function(store) { this.getLoadMoreCmp.setVisible(store.getCount() === 0); }, /** * @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 me = this; if (!me.isAdded) { me.cmp.add(me.getLoadMoreCmp()); me.isAdded = true; me.syncState(); } }, /** * @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.cmp.getStore(), total = store ? store.getTotalCount() : null; return total !== null ? total <= (store.currentPage * store.getPageSize()) : false; }, /** * @private */ loadNextPage: function() { var me = this, list = me.cmp; if (me.storeFullyLoaded()) { return; } me.setLoading(true); me.disableDataViewMask(); me.currentScrollToTopOnRefresh = list.getScrollToTopOnRefresh(); list.setScrollToTopOnRefresh(false); list.getStore().nextPage({ addRecords: true }); }, privates: { loadingCls: Ext.baseCSSPrefix + 'loading', ensureBufferZone: function() { var me = this, list = me.cmp; if (list.isPainted()) { me.ensureBufferZone = me.doEnsureBufferZone; me.doEnsureBufferZone(); return; } if (!me.waitingForPainted) { me.waitingForPainted = true; list.on({ painted: { single: true, fn: function() { delete me.waitingForPainted; me.ensureBufferZone(); } } }); } }, doEnsureBufferZone: function() { var me = this, list = me.cmp, store = list.getStore(), scroller = list.getScrollable(), count = store && store.getCount(), bufferZone = me.getBufferZone(), item, box, y, index; if (!store || !count || !scroller || me.getLoading()) { return; } index = Math.min(Math.max(0, count - bufferZone), count - 1); item = list.mapToItem(store.getAt(index)); box = item && item.element.getBox(); if (!box) { return; } // if bufferZone is 0, loading the next page should happen when reaching the end // of the list (the bottom of the last item), else, if bufferZone is greater than // 0, loading the next page should happen when the first row of pixels of the // leading buffer zone item appears in the view. y = bufferZone > 0 ? box.top + 1 : box.bottom; if (y > scroller.getElement().getBox().bottom) { return; } me.loadNextPage(); }, getLoadTpl: function() { return Ext.XTemplate.getTpl(this, 'loadTpl'); }, syncState: function() { var me = this, list = me.cmp, loadCmp = me.getLoadMoreCmp(), full = me.storeFullyLoaded(), store = list.store, message = full ? me.getNoMoreRecordsText() : me.getLoadMoreText(); if (store && store.getCount()) { loadCmp.show(); } me.setLoading(false); // if we've reached the end of the data set, switch to the noMoreRecordsText loadCmp.setHtml(me.getLoadTpl().apply({ message: message })); loadCmp.setDisabled(full); if (me.currentScrollToTopOnRefresh !== undefined) { list.setScrollToTopOnRefresh(me.currentScrollToTopOnRefresh); delete me.currentScrollToTopOnRefresh; } me.enableDataViewMask(); if (me.getAutoPaging()) { me.ensureBufferZone(); } } }});