import Loader from "./Loader.js"; /** * @class InfiniteScroll * * @description * This object has several resposabilities (maybe too much, anyway...): * * 1. Keeps track of the viewport position in page * 2. Triggers the ajax requests for more contents * 3. Changes the url in the address bar according to the content shown in viewport * */ export class InfiniteScroll { /** * @param { Array } relateds - array of objects * @param { string } container - element selector - where to append new contents * @param { string } content - element selector - content to append, the first is the main content alreay loaded with the page * @param { int } maxLoads - @default 2 * @param { string } loadMoreSelector - the selector that trigger new loadind when gets into viewport @default '#loadmore' * @param { boolean } change_addressbar - if true changes the addressbar @default true * @param { function } onMaxLoads - callback to execute when loadings are finished @default null * */ constructor(relateds = [], container = 'body', content = 'article', preloader = '#wait-loading', maxLoads = 2, loadMoreSelector = '#loadmore', change_addressbar = true, onMaxLoads = null) { this.container = container; // sets the container's selector this.containerElement = document.querySelector(this.container); this.content = content; // sets the content's selector this.relateds = relateds; this.loadedContents = []; // already loaded pages this.loadingContent = { ...relateds.shift(), count: 0 }; this.currentReading = { intersectionRatio: 0, target: null } // set it to false when there's no need to change the url in the address bar this.change_addressbar = change_addressbar; // element to show while loading this.preloader = document.querySelector(preloader); // element that triggers the next content loading this.loadMore = document.querySelector(loadMoreSelector); /// imposta il primo elemento come caricato e gli associa i suoi dati this.setLoadedContent(this.containerElement.querySelector(this.content)); // initializes the current url ... this.current_url = this.loadingContent.url; // `${window.location.protocol}//${window.location.host}${window.location.pathname}`; // ... and the content index in the contents array this.current_content = 0; /// imposta le callback per i caricamenti this.loader = new Loader(this.complete.bind(this), null, this.loading.bind(this)); /// abilita il trigger per il caricamento del uovo contenuto this.enableTrigger(this.loadMore, this.loadNext); this.loadCallbackStack = []; this.viewportCallbackStack = []; } /** * If the user reached the bottom of the content * loadNext tries to load the next one */ loadNext() { // does nothing if loading if (this.loader.loading) return; // ... and if there nothing to load if (this.relateds.length == 0) return; // take the first of the line this.loadingContent = { ...this.relateds.shift(), count: this.loadedContents.length }; // needed to add variable like "is_ajax_request" this.loader.load(`${this.loadingContent.url}?is_ajax_request`); } loading() { this.preloader.style.display = "block"; } complete(payload) { this.preloader.style.display = "none"; let temp = document.createElement('div'); temp.insertAdjacentHTML("beforeend", payload); /// prende il tag che risulta come contenuto let content = temp.querySelector(this.content); /// imposta nel contenuto i data attribute this.setLoadedContent(content); /// sposta tutti i tag che trova while (temp.children.length) { /// add the content to the page this.containerElement.appendChild(temp.removeChild(temp.firstChild)); } /// actions to start for each load this.loadCallbackStack.forEach(callback => callback.call()); // ... and if there is nothing to load, action onMaxLoads if (this.relateds.length == 0) { if (typeof this.onMaxLoads !== 'undefined') this.onMaxLoads(); } } onMaxLoads() { //LATEST LOAD CALLBACK this.observer.unobserve(this.loadMore) } setLoadedContent(content) { /// add some data to the content content.dataset.url = this.loadingContent.url; content.dataset.count = this.loadingContent.count; content.dataset.title = this.loadingContent.title; /// add the content to the loaded ones this.loadedContents.push(this.loadingContent); /// add some actions triggering when content is in viewport this.enableTrigger(content, this.onElementShow) } onElementShow(args) { this.current_content = args.count; this.current_url = args.url; this.current_title = args.title; this.current_tracking_url = ''; if (this.current_url != window.location.href) { this.setAddressBar(); } /// actions to start when the page appears into viewport if (args.count > 0) { this.viewportCallbackStack.forEach(callback => callback.call()); } } // changes the url of the current page in the address bar setAddressBar(title, url) { // changes the values for URL in the address bar window.history.replaceState(null, this.current_title, this.current_url); // updates the title document.title = this.current_title; } /** * set the trigger to sense the loadMore in Viewport * and execute the callback */ enableTrigger(trigger, callback) { trigger.callback = callback.bind(this); const options = { root: document.querySelector('main-container'), threshold: Array.from({ length: 100 }, (_, index) => index / 100) }; /// setup the observer this.observer = new IntersectionObserver((entries, observer) => { entries.forEach(entry => { if (this.currentReading.intersectionRatio < entry.intersectionRatio) { entry.target.callback(entry.target.dataset); /// call the callback (entry.target.id != 'loadmore') ? (this.currentReading = entry) : null; } if (this.currentReading.target == entry.target) { this.currentReading = entry } }); }, options); /// start observing this.observer.observe(trigger); } }