1(function ($) { 2 'use strict'; 3 4 class AjaxLoadMore { 5 constructor() { 6 this.container = $('.ajax-load-more-container'); 7 this.postsContainer = $('.ajax-load-more-posts'); 8 this.loadBtn = $('.ajax-load-more-btn'); 9 this.loading = $('.ajax-load-more-loading'); 10 this.noMore = $('.ajax-load-more-no-more'); 11 12 const urlPage = this.getPageFromURL(); 13 this.targetPage = urlPage; 14 this.currentPage = 1; 15 this.maxPages = parseInt(this.container.data('max-pages')) || 1; 16 this.postsPerPage = parseInt(this.container.data('posts-per-page')) || 10; 17 this.autoLoadOnce = false; 18 this.scrollThreshold = 0; 19 // this.autoLoadOnce = ajax_load_more_params.auto_load_once; 20 // this.scrollThreshold = ajax_load_more_params.scroll_threshold || 200; 21 this.autoScrollEnabled = this.container.data('auto-scroll') !== false; 22 23 this.isLoading = false; 24 this.autoLoaded = false; 25 this.scrollTimeout = null; 26 this.urlUpdateTimeout = null; 27 28 this.init(); 29 } 30 31 init() { 32 if (this.container.length === 0) return; 33 34 this.bindEvents(); 35 this.handleInitialLoad(); 36 this.restoreScrollPosition(); 37 38 if (this.currentPage < this.maxPages) { 39 this.loadBtn.show(); 40 } 41 } 42 43 bindEvents() { 44 this.loadBtn.on('click', (e) => { 45 e.preventDefault(); 46 this.loadMorePosts(); 47 }); 48 49 $(window).on( 50 'scroll.ajaxLoadMore', 51 this.throttle(() => { 52 this.handleScroll(); 53 }, 250) 54 ); 55 56 $(window).on('popstate', (e) => { 57 if (e.originalEvent.state && e.originalEvent.state.page) { 58 this.handlePopState(e.originalEvent.state); 59 } 60 }); 61 } 62 63 handleInitialLoad() { 64 const urlPage = this.getPageFromURL(); 65 66 this.currentPage = urlPage; 67 this.updateURL(urlPage, false); 68 69 this.updateUI(); 70 } 71 72 async loadPreviousPages(targetPage) { 73 for (let page = 2; page <= targetPage; page++) { 74 while (this.isLoading) { 75 await new Promise((resolve) => setTimeout(resolve, 50)); 76 } 77 78 await this.loadPage(page, true); 79 80 if (page < targetPage) { 81 await new Promise((resolve) => setTimeout(resolve, 100)); 82 } 83 } 84 85 this.currentPage = targetPage; 86 this.updateURL(targetPage, false); 87 this.updateUI(); 88 } 89 90 handleScroll() { 91 if ( 92 this.isLoading || 93 this.currentPage >= this.maxPages || 94 !this.autoScrollEnabled 95 ) 96 return; 97
98 const loadMoreBtn = this.loadBtn.length 99 ? this.loadBtn 100 : this.container.find('.ajax-load-more-controls'); 101 102 if (loadMoreBtn.length === 0) return; 103 104 const buttonTop = loadMoreBtn.offset().top; 105 const scrollTop = $(window).scrollTop(); 106 const windowHeight = $(window).height(); 107 const viewportBottom = scrollTop + windowHeight; 108 109 const distanceToButton = buttonTop - viewportBottom; 110 111 const effectiveThreshold = 112 this.scrollThreshold + (this.isLoading ? 500 : 0); 113 const shouldTriggerLoad = 114 distanceToButton <= effectiveThreshold && distanceToButton > -100; 115 116 if (this.autoLoadOnce && !this.autoLoaded && shouldTriggerLoad) { 117 this.autoLoaded = true; 118 this.loadMorePosts(); 119 return; 120 } 121 122 if (!this.autoLoadOnce && shouldTriggerLoad) { 123 this.loadMorePosts(); 124 } 125 126 this.updateURLBasedOnScroll(); 127 } 128 129 updateURLBasedOnScroll() { 130 return; 131 /* 132 if (this.urlUpdateTimeout) { 133 clearTimeout(this.urlUpdateTimeout); 134 } 135 136 this.urlUpdateTimeout = setTimeout(() => { 137 const scrollTop = $(window).scrollTop(); 138 const items = this.postsContainer.find('.ajax-load-more-item'); 139 let currentVisiblePage = 1; 140 141 items.each((index, item) => { 142 const itemTop = $(item).offset().top; 143 if (scrollTop >= itemTop - 100) { 144 currentVisiblePage = Math.ceil((index + 1) / this.postsPerPage); 145 } 146 }); 147 148 if (currentVisiblePage !== this.getPageFromURL()) { 149 this.updateURL(currentVisiblePage, false); 150 this.storeScrollPosition(); 151 } 152 }, 500); 153 */ 154 } 155 156 async loadMorePosts() { 157 if (this.isLoading || this.currentPage >= this.maxPages) return; 158 159 const nextPage = this.currentPage + 1; 160 161 if (nextPage > this.maxPages) { 162 this.updateUI(); 163 return; 164 } 165 166 await this.loadPage(nextPage); 167 } 168 169 async loadPage(page, silent = false) { 170 if (this.isLoading) { 171 return; 172 } 173 174 this.isLoading = true; 175 176 if (!silent) { 177 this.showLoading(); 178 } 179 180 // Check if this is the home page with special layout 181 const isHome = $('body').hasClass('home'); 182 183 const data = { 184 action: isHome ? 'load_more_home_posts' : 'load_more_posts', 185 nonce: ajax_load_more_params.nonce, 186 page: page, 187 posts_per_page: this.postsPerPage, 188 post_type: this.container.data('post-type') || 'post', 189 category: this.container.data('category') || '', 190 tag: this.container.data('tag') || '', 191 meta_key: this.container.data('meta-key') || '', 192 meta_value: this.container.data('meta-value') || '', 193 hide_category: this.container.attr('data-hide-category') || '0', 194 }; 195 196 const containerData = this.container.data(); 197 Object.keys(containerData).forEach((key) => { 198 if (key.startsWith('query')) { 199 const cleanKey = key.replace('query', '').toLowerCase(); 200 data[cleanKey] = containerData[key]; 201 } 202 }); 203 204 try { 205 const response = await $.ajax({ 206 url: ajax_load_more_params.ajax_url, 207 type: 'POST', 208 data: data, 209 }); 210 211 if (response.success) { 212 if ( 213 !response.data.content || 214 response.data.content.trim().length === 0 215 ) { 216 this.currentPage = this.maxPages; 217 this.updateUI(); 218 this.isLoading = false; 219 this.hideLoading(); 220 return; 221 } 222 223 const newContent = $(response.data.content); 224 this.postsContainer.append(newContent); 225 226 this.currentPage = page; 227 228 if ( 229 response.data.max_pages && 230 response.data.max_pages !== this.maxPages 231 ) { 232 this.maxPages = response.data.max_pages; 233 this.container.attr('data-max-pages', this.maxPages); 234 } 235 236 if (!silent) { 237 this.updateURL(page, true); 238 this.storeScrollPosition(); 239 } 240 241 const customEvent = new CustomEvent('ajax_load_more:loaded', { 242 detail: { 243 newContent: newContent.toArray(), 244 newContentEl: newContent, 245 page: page, 246 }, 247 }); 248 document.dispatchEvent(customEvent); 249
250 $(document).trigger('ajax_load_more:loaded', [newContent, page]); 251 252 this.updateUI(); 253 254 setTimeout(() => { 255 this.isLoading = false; 256 this.hideLoading(); 257 }, 500); 258 } else { 259 if (this.currentPage >= this.maxPages) { 260 this.updateUI(); 261 } else { 262 this.showNoMore(); 263 } 264 } 265 266 setTimeout(() => { 267 this.isLoading = false; 268 this.hideLoading(); 269 }, 500); 270 } catch (error) { 271 if (error.responseText) { 272 console.error('ð Server response:', error.responseText); 273 } 274 275 if (this.currentPage >= this.maxPages) { 276 this.updateUI(); 277 } else { 278 this.showNoMore(); 279 } 280 } 281 282 setTimeout(() => { 283 this.isLoading = false; 284 this.hideLoading(); 285 }, 500); 286 } 287 288 updateUI() { 289 if (this.currentPage >= this.maxPages) { 290 this.loadBtn.hide(); 291 this.showNoMore(); 292 293 $(window).off('scroll.ajaxLoadMore'); 294 return; 295 } 296 297 if (this.autoLoadOnce && this.autoLoaded) { 298 this.loadBtn.show(); 299 } else if (!this.autoLoadOnce) { 300 this.loadBtn.show(); 301 } 302 } 303 304 showLoading() { 305 this.loadBtn.hide(); 306 this.loading.show(); 307 } 308 309 hideLoading() { 310 this.loading.hide(); 311 } 312 313 showNoMore() { 314 this.loadBtn.hide(); 315 this.noMore.show(); 316 } 317 318 updateURL(page, pushState = true) { 319 const url = this.buildURL(page); 320 const state = { 321 page: page, 322 scroll_pos: $(window).scrollTop(), 323 }; 324 325 if (pushState) { 326 history.pushState(state, '', url); 327 } else { 328 history.replaceState(state, '', url); 329 } 330 } 331 332 buildURL(page) { 333 const searchParams = new URLSearchParams(window.location.search); 334 335 // For search pages, use ?s=term&paged=N format 336 if (searchParams.has('s')) { 337 if (page > 1) { 338 searchParams.set('paged', page); 339 } else { 340 searchParams.delete('paged'); 341 } 342 return `${window.location.pathname}?${searchParams.toString()}`; 343 } 344 345 // For other pages, use /page/N/ format 346 let baseUrl = window.location.pathname.replace(/\/page\/\d+\/?$/, ''); 347 if (baseUrl.endsWith('/')) { 348 baseUrl = baseUrl.slice(0, -1); 349 } 350 return page > 1 ? `${baseUrl}/page/${page}/` : baseUrl; 351 } 352 353 getPageFromURL() { 354 // Check for ?paged=N (search pages) 355 const searchParams = new URLSearchParams(window.location.search); 356 if (searchParams.has('paged')) { 357 return parseInt(searchParams.get('paged')) || 1; 358 } 359 360 // Check for /page/N/ (other pages) 361 const match = window.location.pathname.match(/\/page\/(\d+)\/?$/); 362 return match ? parseInt(match[1]) : 1; 363 } 364 365 storeScrollPosition() { 366 sessionStorage.setItem( 367 'ajax_load_more_scroll_pos', 368 $(window).scrollTop() 369 ); 370 sessionStorage.setItem('ajax_load_more_page', this.currentPage); 371 } 372
373 waitForContentAndRestoreScroll(targetScrollPos, targetPage) { 374 const expectedItems = targetPage * this.postsPerPage; 375 let attempts = 0; 376 const maxAttempts = 20; 377 378 const checkAndRestore = () => { 379 const currentItems = this.postsContainer.find( 380 '.ajax-load-more-item' 381 ).length; 382 attempts++; 383 384 if (currentItems >= expectedItems || attempts >= maxAttempts) { 385 setTimeout(() => { 386 if (targetScrollPos) { 387 $(window).scrollTop(targetScrollPos); 388 } else { 389 const items = this.postsContainer.find('.ajax-load-more-item'); 390 const startIndex = (targetPage - 1) * this.postsPerPage; 391 392 if (items.length > startIndex) { 393 const targetItem = items.eq(startIndex); 394 if (targetItem.length) { 395 $(window).scrollTop(targetItem.offset().top - 100); 396 } 397 } 398 } 399 }, 100); 400 } else { 401 setTimeout(checkAndRestore, 200); 402 } 403 }; 404 405 checkAndRestore(); 406 } 407 408 restoreScrollPosition() { 409 const urlPage = this.getPageFromURL(); 410 411 if (urlPage > 1) { 412 setTimeout(() => { 413 const items = this.postsContainer.find('.ajax-load-more-item'); 414 const targetItemIndex = (urlPage - 1) * this.postsPerPage; 415 416 if (items.length > targetItemIndex) { 417 const targetItem = items.eq(targetItemIndex); 418 if (targetItem.length) { 419 $('html, body').animate( 420 { 421 scrollTop: targetItem.offset().top - 100, 422 }, 423 500 424 ); 425 } 426 } 427 }, 500); 428 429 return; 430 } 431 432 const scrollPos = sessionStorage.getItem('ajax_load_more_scroll_pos'); 433 const storedPage = sessionStorage.getItem('ajax_load_more_page'); 434 435 if ( 436 scrollPos && 437 storedPage && 438 parseInt(storedPage) === this.currentPage 439 ) { 440 setTimeout(() => { 441 $(window).scrollTop(parseInt(scrollPos)); 442 }, 100); 443 } 444 } 445 446 handlePopState(state) { 447 const targetPage = this.getPageFromURL(); 448 const targetScrollPos = state ? state.scroll_pos : null; 449 450 if (targetPage < this.currentPage) { 451 window.location.reload(); 452 } else if (targetPage > this.currentPage) { 453 this.loadToPage(targetPage).then(() => {
454 this.waitForContentAndRestoreScroll(targetScrollPos, targetPage); 455 }); 456 } else if (targetScrollPos) { 457 setTimeout(() => { 458 $(window).scrollTop(targetScrollPos); 459 }, 100); 460 } 461 } 462 463 async loadToPage(targetPage) { 464 while ( 465 this.currentPage < targetPage && 466 this.currentPage < this.maxPages 467 ) { 468 const nextPage = this.currentPage + 1; 469 await this.loadPage(nextPage, true); 470 } 471 472 this.updateUI(); 473 } 474 475 throttle(func, limit) { 476 let inThrottle; 477 return function () { 478 const args = arguments; 479 const context = this; 480 if (!inThrottle) { 481 func.apply(context, args); 482 inThrottle = true; 483 setTimeout(() => (inThrottle = false), limit); 484 } 485 }; 486 } 487 } 488 489 $(document).ready(function () { 490 new AjaxLoadMore(); 491 }); 492})(jQuery);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.