diff --git a/src/blocks/homepage-articles/block.json b/src/blocks/homepage-articles/block.json index de26216cc..de51385f1 100644 --- a/src/blocks/homepage-articles/block.json +++ b/src/blocks/homepage-articles/block.json @@ -18,6 +18,10 @@ "type": "boolean", "default": false }, + "infiniteScroll": { + "type": "boolean", + "default": false + }, "readMoreLabel": { "type": "string", "default": "Keep reading" diff --git a/src/blocks/homepage-articles/edit.tsx b/src/blocks/homepage-articles/edit.tsx index 476a1b3a2..f6b4d0eb8 100644 --- a/src/blocks/homepage-articles/edit.tsx +++ b/src/blocks/homepage-articles/edit.tsx @@ -280,6 +280,7 @@ class Edit extends Component< HomepageArticlesProps > { mobileStack, minHeight, moreButton, + infiniteScroll, showExcerpt, showReadMore, readMoreLabel, @@ -434,11 +435,20 @@ class Edit extends Component< HomepageArticlesProps > { ) : ( ! specificMode && ( - setAttributes( { moreButton: ! moreButton } ) } - /> + <> + setAttributes( { moreButton: ! moreButton } ) } + /> + { moreButton && ( + setAttributes( { infiniteScroll: ! infiniteScroll } ) } + /> + ) } + ) ) } { - // Early return if still fetching or no more posts to render. - if ( isFetching || isEndOfData ) { - return false; + const maybeLoadMore = () => { + if ( isPending ) { + return; } + isPending = true; + loadMore(); + }; - isFetching = true; + const loadMore = () => { + // Early return if no more posts to render. + if ( isEndOfData ) { + return false; + } blockWrapperEl.classList.remove( 'is-error' ); blockWrapperEl.classList.add( 'is-loading' ); @@ -55,48 +64,86 @@ function buildLoadMoreHandler( blockWrapperEl ) { const requestURL = btnEl.getAttribute( 'data-next' ) + '&exclude_ids=' + getRenderedPostsIds().join( ',' ); + // If there's already a fetch in progress, queue this one to run after it ends. + if ( window.newspackBlocksIsFetching ) { + window.newspackBlocksFetchQueue.push( loadMore ); + return false; + } + + window.newspackBlocksIsFetching = true; fetchWithRetry( { url: requestURL, onSuccess, onError }, fetchRetryCount ); + }; - /** - * @param {Object} data Post data - */ - function onSuccess( data ) { - // Validate received data. - if ( ! isPostsDataValid( data ) ) { - return onError(); - } + /** + * @param {Object} data Post data + */ + function onSuccess( data ) { + // Validate received data. + if ( ! isPostsDataValid( data ) ) { + return onError(); + } - if ( data.items.length ) { - // Render posts' HTML from string. - const postsHTML = data.items.map( item => item.html ).join( '' ); - postsContainerEl.insertAdjacentHTML( 'beforeend', postsHTML ); - } + if ( data.items.length ) { + // Render posts' HTML from string. + const postsHTML = data.items.map( item => item.html ).join( '' ); + postsContainerEl.insertAdjacentHTML( 'beforeend', postsHTML ); + } - if ( data.next ) { - // Save next URL as button's attribute. - btnEl.setAttribute( 'data-next', data.next ); - } + if ( data.next ) { + // Save next URL as button's attribute. + btnEl.setAttribute( 'data-next', data.next ); + } - if ( ! data.items.length || ! data.next ) { - isEndOfData = true; - blockWrapperEl.classList.remove( 'has-more-button' ); - } + if ( ! data.items.length || ! data.next ) { + isEndOfData = true; + blockWrapperEl.classList.remove( 'has-more-button' ); + } - isFetching = false; + onEnd(); + } - blockWrapperEl.classList.remove( 'is-loading' ); - } + /** + * Handle fetching error + */ + function onError() { + blockWrapperEl.classList.add( 'is-error' ); + onEnd(); + } - /** - * Handle fetching error - */ - function onError() { - isFetching = false; + /** + * Callback to run after a fetch request is completed. + */ + function onEnd() { + window.newspackBlocksIsFetching = false; + blockWrapperEl.classList.remove( 'is-loading' ); - blockWrapperEl.classList.remove( 'is-loading' ); - blockWrapperEl.classList.add( 'is-error' ); + // If there are queued fetches, run the next one. + if ( window.newspackBlocksFetchQueue.length ) { + window.newspackBlocksFetchQueue.shift()(); } - } ); + isPending = false; + } + + btnEl.addEventListener( 'click', maybeLoadMore ); + + if ( isInfiniteScroll ) { + // Create an intersection observer instance + const btnObserver = new IntersectionObserver( + entries => { + entries.forEach( entry => { + if ( entry.isIntersecting ) { + maybeLoadMore(); + } + } ); + }, + { + root: null, + rootMargin: '0px', + threshold: 1, + } + ); + btnObserver.observe( btnEl ); + } } /** diff --git a/src/blocks/homepage-articles/view.php b/src/blocks/homepage-articles/view.php index 5aa08a409..f55a1c48c 100644 --- a/src/blocks/homepage-articles/view.php +++ b/src/blocks/homepage-articles/view.php @@ -383,8 +383,12 @@ class="" -