PageSourceSearch

https://medcitynews.com/wp-content/plugins/bm-load-more-plugin/assets/js/load-more.js?ver=1.0.0

js medcitynews.com collected 2026-09-24 08:32:26 UTC 20,604 bytes, 699 lines download raw bytes

1/**
2 * BM Load More JavaScript
3 *
4 * Handles load more functionality with accessibility and URL management.
5 *
6 * @package BMLoadMore
7 */
8
9(function() {
10	'use strict';
11
12	// Check if required data is available.
13	if (typeof bmLoadMore === 'undefined') {
14		return;
15	}
16
17	const LoadMore = {
18		container: null,
19		button: null,
20		currentPage: parseInt(bmLoadMore.page, 10) || 1,
21		maxPages: 0,
22		isLoading: false,
23		context: bmLoadMore.context,
24		endpoint: bmLoadMore.endpoint,
25		nonce: bmLoadMore.nonce,
26		i18n: bmLoadMore.i18n,
27
28		/**
29		 * Initialize load more functionality.
30		 */
31		init: function() {
32			// Check if we should auto-inject button (homepage/frontpage or archive).
33			const shouldAutoInject = this.shouldAutoInjectButton();
34
35			if (shouldAutoInject) {
36				// Auto-inject button into #content container.
37				this.autoInjectButton();
38			}
39
40			// Find existing button or injected button.
41			this.container = document.querySelector('.bm-load-more-container');
42			this.button = document.querySelector('.bm-load-more-button');
43
44			if (!this.button) {
45				return;
46			}
47
48			// Get max pages from localized script data.
49			if (bmLoadMore.maxPages && bmLoadMore.maxPages > 0) {
50				this.maxPages = parseInt(bmLoadMore.maxPages, 10);
51			}
52
53			// Also check data attribute on posts container as fallback.
54			const postsContainer = document.querySelector('.posts-container, .archive-posts, main .posts, #main .posts');
55			if (postsContainer && !this.maxPages) {
56				const maxPagesAttr = postsContainer.getAttribute('data-max-pages');
57				if (maxPagesAttr) {
58					this.maxPages = parseInt(maxPagesAttr, 10);
59				}
60			}
61
62			// Bind events.
63			this.button.addEventListener('click', this.handleClick.bind(this));
64
65			// Keyboard support.
66			this.button.addEventListener('keydown', this.handleKeydown.bind(this));
67
68			// Update initial state.
69			this.updateButtonState();
70		},
71
72		/**
73		 * Check if button should be auto-injected.
74		 * Only on homepage/frontpage or archive pages.
75		 *
76		 * @return {boolean} True if should auto-inject.
77		 */
78		shouldAutoInjectButton: function() {
79			// Check context type from localized data.
80			if (bmLoadMore.context && bmLoadMore.context.type) {
81				const contextType = bmLoadMore.context.type;
82				// Homepage, frontpage, or any archive type.
83				return contextType === 'home' ||
84				       contextType === 'author' ||
85				       contextType === 'category' ||
86				       contextType === 'tag' ||
87				       contextType === 'taxonomy' ||
88				       contextType === 'post_type_archive' ||
89				       contextType === 'date' ||
90				       contextType === 'search';
91			}
92			return false;
93		},
94
95		/**
96		 * Auto-inject button into #content container.
97		 */
98		autoInjectButton: function() {
99			// Find #content container.
100			const contentContainer = document.getElementById('content');
101			if (!contentContainer) {
102				return;
103			}
104
105			// Check if button already exists.
106			if (document.querySelector('.bm-load-more-container')) {
107				return;
108			}
109
110			// Check if there are more pages available.
111			const maxPages = bmLoadMore.maxPages || 0;
112			const currentPage = bmLoadMore.page || 1;
113			const postsPerPage = bmLoadMore.postsPerPage || 10;
114			const currentPostCount = bmLoadMore.currentPostCount || 0;
115
116			// If maxPages is 0 (no_found_rows is true), check if we have a full page of results.
117			if (Number(maxPages) === 0) {
118				// Show button if we have a full page of results (suggests more might exist). compare integer values
119				if ( Number(currentPostCount) < Number(postsPerPage) ) {
120					// Less than full page, no more results.
121					return;
122				}
123				// Full page of results, show button (we'll hide it if next load returns fewer).
124			} else {
125				// Normal pagination - check max pages.
126				if ( Number(currentPage) >= Number(maxPages) ) {
127					// No more pages, don't inject button.
128					return;
129				}
130			}
131
132			// Create button markup.
133			const buttonMarkup = this.getButtonMarkup();
134
135			// Insert at the end of #content.
136			contentContainer.insertAdjacentHTML('beforeend', buttonMarkup);
137		},
138
139		/**
140		 * Get button markup HTML.
141		 *
142		 * @return {string} Button HTML markup.
143		 */
144		getButtonMarkup: function() {
145			const text = bmLoadMore.i18n.loadMore || 'Load More';
146			const loadingText = bmLoadMore.i18n.loading || 'Loading...';
147
148			return '<div class="bm-load-more-container">' +
149				'<button type="button" class="bm-load-more-button" aria-label="' + this.escapeHtml(text) + '" aria-busy="false">' +
150				'<span class="button-text">' + this.escapeHtml(text) + '</span>' +
151				'<span class="button-loading" aria-hidden="true" style="display: none;">' + this.escapeHtml(loadingText) + '</span>' +
152				'</button>' +
153				'</div>';
154		},
155
156		/**
157		 * Escape HTML to prevent XSS.
158		 *
159		 * @param {string} text Text to escape.
160		 * @return {string} Escaped text.
161		 */
162		escapeHtml: function(text) {
163			const div = document.createElement('div');
164			div.textContent = text;
165			return div.innerHTML;
166		},
167
168		/**
169		 * Handle button click.
170		 *
171		 * @param {Event} e Click event.
172		 */
173		handleClick: function(e) {
174			e.preventDefault();
175
176			if (this.isLoading || !this.hasMore()) {
177				return;
178			}
179
180			this.loadMore();
181		},
182
183		/**
184		 * Handle keyboard events.
185		 *
186		 * @param {Event} e Keyboard event.
187		 */
188		handleKeydown: function(e) {
189			// Enter or Space key.
190			if (e.key === 'Enter' || e.key === ' ') {
191				if (e.key === ' ') {
192					e.preventDefault();
193				}
194				this.handleClick(e);
195			}
196		},
197
198		/**
199		 * Check if there are more pages to load.
200		 *
201		 * @return {boolean} True if more pages available.
202		 */
203		hasMore: function() {
204			if (this.maxPages > 0) {
205				return this.currentPage < this.maxPages;
206			}
207			// If max pages is 0 (no_found_rows), we'll determine based on results.
208			// This will be handled in handleSuccess when we see how many posts were returned.
209			return true;
210		},
211
212		/**
213		 * Load more posts.
214		 */
215		loadMore: function() {
216			if (this.isLoading) {
217				return;
218			}
219
220			this.isLoading = true;
221			this.currentPage += 1;
222
223			// Clear any existing sessionStorage for this page (in case of retry).
224			const urlKey = 'bm_load_more_url_updated_' + this.currentPage;
225			sessionStorage.removeItem(urlKey);
226
227			// Update button state.
228			this.setLoadingState(true);
229
230			// Prepare form data (POST to current page like Jetpack).
231			const formData = new FormData();
232			formData.append('bm_load_more', '1');
233			formData.append('nonce', this.nonce);
234			formData.append('context', JSON.stringify(this.context));
235			formData.append('page', this.currentPage);
236
237			// Debug logging. // @todo remove after testing on staging
238			console.log('BM Load More - Endpoint:', this.endpoint);
239			console.log('BM Load More - Context:', this.context);
240			console.log('BM Load More - Page:', this.currentPage);
241
242			// Make POST request to current page.
243			fetch(this.endpoint, {
244				method: 'POST',
245				body: formData,
246				credentials: 'same-origin',
247			})
248				.then(response => {
249					if (!response.ok) {
250						throw new Error('Network response was not ok');
251					}
252					return response.json();
253				})
254				.then(response => {
255					// Handle wp_send_json_success format: {success: true, data: {...}}
256					if (response && response.success && response.data) {
257						this.handleSuccess(response.data);
258					} else if (response && !response.success) {
259						// Handle wp_send_json_error format: {success: false, data: {...}}
260						const errorMessage = (response.data && response.data.message) 
261							? response.data.message 
262							: 'Error loading posts. Please try again.';
263						throw new Error(errorMessage);
264					} else {
265						// Fallback: assume response is data directly.
266						this.handleSuccess(response);
267					}
268				})
269				.catch(error => {
270					this.handleError(error);
271				});
272		},
273
274		/**
275		 * Handle successful response.
276		 *
277		 * @param {Object} data Response data.
278		 */
279		handleSuccess: function(data) {
280			this.isLoading = false;
281			this.setLoadingState(false);
282
283			// Check if we have HTML content.
284			if (data.html && data.html.trim() !== '') {
285				// Insert HTML markup.
286				const insertedElements = this.insertHTML(data.html);
287
288				// Update max pages from response.
289				if (data.max_pages !== undefined) {
290					this.maxPages = parseInt(data.max_pages, 10);
291				}
292
293				// Count posts in the HTML for announcement and to determine if more exist.
294				const tempDiv = document.createElement('div');
295				tempDiv.innerHTML = data.html;
296				const postCount = tempDiv.querySelectorAll('article, .post, [class*="post-"]').length;
297				const postsPerPage = bmLoadMore.postsPerPage || 10;
298
299				// If maxPages is 0 (no_found_rows), check if we got a full page of results.
300				// If we got fewer than posts_per_page, we've reached the end.
301				if (this.maxPages === 0 && postCount < postsPerPage) {
302					// Fewer results than posts_per_page means no more results.
303					// Set maxPages to current page to prevent further loads.
304					this.maxPages = this.currentPage;
305				}
306
307				// Set up scroll-based URL update (like Jetpack).
308				// URL will update when user scrolls to the new content.
309				// Use the wrapper's unique ID for reliable detection.
310				const wrapperId = 'archive__content-' + this.currentPage;
311				setTimeout(() => {
312					const wrapperElement = document.getElementById(wrapperId);
313					if (wrapperElement) {
314						this.setupScrollBasedURLUpdate(wrapperElement, this.currentPage);
315					}
316				}, 100);
317
318				// Announce to screen readers.
319				this.announceToScreenReader(
320					this.i18n.loadMore + ': ' + postCount + ' ' +
321					(postCount === 1 ? 'post loaded' : 'posts loaded')
322				);
323			} else {
324				// No HTML content - definitely no more results.
325				// Update max pages if provided, otherwise set to current page.
326				if (data.max_pages !== undefined) {
327					this.maxPages = parseInt(data.max_pages, 10);
328				} else {
329					// No content returned, we've reached the end.
330					this.maxPages = this.currentPage;
331				}
332			}
333
334			// Update button state.
335			this.updateButtonState();
336
337			// Trigger custom event.
338			const event = new CustomEvent('bmLoadMoreLoaded', {
339				detail: {
340					page: this.currentPage,
341					html: data.html || '',
342					hasMore: data.has_more || false,
343				},
344			});
345			document.dispatchEvent(event);
346		},
347
348		/**
349		 * Handle error.
350		 *
351		 * @param {Error} error Error object.
352		 */
353		handleError: function(error) {
354			this.isLoading = false;
355			this.setLoadingState(false);
356			this.currentPage -= 1; // Revert page increment.
357
358			// Show error message.
359			this.showError(this.i18n.error);
360
361			// Announce to screen readers.
362			this.announceToScreenReader(this.i18n.error);
363
364			// Trigger custom event.
365			const event = new CustomEvent('bmLoadMoreError', {
366				detail: {
367					error: error,
368				},
369			});
370			document.dispatchEvent(event);
371		},
372
373		/**
374		 * Insert HTML markup into the page.
375		 *
376		 * @param {string} html HTML markup string.
377		 * @return {Array} Array of inserted elements.
378		 */
379		insertHTML: function(html) {
380			// Find the bm-load-more-container to insert before it.
381			const loadMoreContainer = document.querySelector('.bm-load-more-container');
382
383			if (!loadMoreContainer) {
384				console.warn('BM Load More: Load more container not found');
385				return null;
386			}
387
388			// Create temporary container for parsing HTML.
389			const tempDiv = document.createElement('div');
390			tempDiv.innerHTML = html;
391
392			// Track inserted elements.
393			const insertedElements = [];
394
395			// Insert all content from tempDiv just before the load more container.
396			while (tempDiv.firstChild) {
397				const insertedElement = loadMoreContainer.parentNode.insertBefore(tempDiv.firstChild, loadMoreContainer);
398				insertedElements.push(insertedElement);
399			}
400
401			// Trigger lazy loading if available.
402			if (typeof window.lazyLoadInstance !== 'undefined') {
403				window.lazyLoadInstance.update();
404			}
405
406			return insertedElements;
407		},
408
409		/**
410		 * Set up scroll-based URL update (like Jetpack).
411		 * URL will update when user scrolls to the new content.
412		 *
413		 * @param {Element} targetElement First element of newly loaded content.
414		 * @param {number} page Page number to update URL to.
415		 */
416		setupScrollBasedURLUpdate: function(targetElement, page) {
417			if (!targetElement) {
418				console.warn('BM Load More: No target element for URL update');
419				return;
420			}
421
422			// Check if URL was already updated for this page.
423			const urlKey = 'bm_load_more_url_updated_' + page;
424			if (sessionStorage.getItem(urlKey)) {
425				console.log('BM Load More: URL already updated for page', page);
426				return; // Already updated for this page.
427			}
428
429			// Check if element is already visible when inserted.
430			const checkIfVisible = () => {
431				const rect = targetElement.getBoundingClientRect();
432				const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
433				// Element is visible if it's within the viewport (with some margin).
434				const isVisible = rect.top < viewportHeight && rect.bottom > 0;
435				return isVisible;
436			};
437
438			// Function to update URL and mark as done.
439			const performURLUpdate = () => {
440				if (!sessionStorage.getItem(urlKey)) {
441					console.log('BM Load More: Attempting URL update to page', page);
442					// Update URL first, then mark as done only if successful.
443					const currentUrl = window.location.href;
444					this.updateURL(page);
445					// Check if URL actually changed (give it a moment).
446					setTimeout(() => {
447						const newUrl = window.location.href;
448						if (newUrl !== currentUrl) {
449							console.log('BM Load More: URL successfully updated to:', newUrl);
450							sessionStorage.setItem(urlKey, 'true');
451						} else {
452							console.warn('BM Load More: URL update may have failed. Current URL:', currentUrl);
453						}
454					}, 50);
455					return true;
456				} else {
457					console.log('BM Load More: URL update already attempted for page', page);
458					return false;
459				}
460			};
461
462			// If already visible, update URL immediately.
463			if (checkIfVisible()) {
464				console.log('BM Load More: Element already visible, updating URL immediately');
465				performURLUpdate();
466				return;
467			}
468
469			// Store observer and scroll handler references for cleanup.
470			let observer = null;
471			let scrollHandler = null;
472
473			// Fallback: If user scrolls manually, check on scroll.
474			scrollHandler = () => {
475				if (checkIfVisible()) {
476					if (performURLUpdate()) {
477						// Clean up after successful update.
478						window.removeEventListener('scroll', scrollHandler);
479						if (observer) {
480							observer.disconnect();
481						}
482					}
483				}
484			};
485
486			// Use Intersection Observer to detect when element comes into view.
487			if ('IntersectionObserver' in window) {
488				observer = new IntersectionObserver(
489					(entries) => {
490						entries.forEach((entry) => {
491							if (entry.isIntersecting) {
492								// Element is now visible, update URL.
493								if (performURLUpdate()) {
494									// Stop observing once URL is updated.
495									observer.unobserve(entry.target);
496									window.removeEventListener('scroll', scrollHandler);
497									observer.disconnect();
498								}
499							}
500						});
501					},
502					{
503						// Trigger when any part of element is visible.
504						threshold: 0,
505						// Start checking when element is 200px from viewport.
506						rootMargin: '200px 0px',
507					}
508				);
509
510				// Start observing the wrapper element.
511				observer.observe(targetElement);
512			}
513
514			// Always add scroll listener as fallback (in addition to IntersectionObserver).
515			window.addEventListener('scroll', scrollHandler, { passive: true });
516		},
517
518		/**
519		 * Update URL without page reload.
520		 * Handles WordPress permalink structure (/page/2/) and query parameters.
521		 *
522		 * @param {number} page Page number.
523		 */
524		updateURL: function(page) {
525			try {
526				const currentUrl = window.location.href;
527				const url = new URL(currentUrl);
528				const pathname = url.pathname;
529
530				if (page <= 1) {
531					// For page 1, remove pagination from URL.
532					// Remove /page/X/ from pathname.
533					const newPathname = pathname.replace(/\/page\/\d+\/?$/, '');
534					url.pathname = newPathname || '/';
535
536					// Also remove paged from query params if present.
537					url.searchParams.delete('paged');
538
539					const newUrl = url.toString();
540					console.log('BM Load More: Updating URL to page 1');
541					console.log('BM Load More: From:', currentUrl);
542					console.log('BM Load More: To:', newUrl);
543					
544					window.history.replaceState(
545						{ page: 1 },
546						'',
547						newUrl
548					);
549					return;
550				}
551
552				// Check if URL already has /page/X/ pattern.
553				const pagePattern = /\/page\/\d+\/?$/;
554				let newPathname;
555
556				if (pagePattern.test(pathname)) {
557					// Replace existing /page/X/ with new page number.
558					newPathname = pathname.replace(pagePattern, '/page/' + page + '/');
559				} else {
560					// Add /page/X/ to pathname.
561					// Ensure pathname ends with / before adding page.
562					const cleanPath = pathname.endsWith('/') ? pathname : pathname + '/';
563					newPathname = cleanPath + 'page/' + page + '/';
564				}
565
566				url.pathname = newPathname;
567
568				// Keep existing query parameters (they're preserved automatically in URL object).
569				// Remove paged from query params if it exists (we use pathname instead).
570				url.searchParams.delete('paged');
571
572				const newUrl = url.toString();
573				console.log('BM Load More: Updating URL to page', page);
574				console.log('BM Load More: From:', currentUrl);
575				console.log('BM Load More: To:', newUrl);
576				console.log('BM Load More: Pathname changed from', pathname, 'to', newPathname);
577				
578				// Update URL without reload.
579				window.history.pushState(
580					{ page: page },
581					'',
582					newUrl
583				);
584				
585				// Verify the URL was actually updated.
586				const verifyUrl = window.location.href;
587				if (verifyUrl === newUrl) {
588					console.log('BM Load More: URL successfully updated');
589				} else {
590					console.warn('BM Load More: URL may not have updated. Expected:', newUrl, 'Got:', verifyUrl);
591				}
592			} catch (error) {
593				console.error('BM Load More: Error updating URL:', error);
594			}
595		},
596
597		/**
598		 * Set loading state.
599		 *
600		 * @param {boolean} loading Whether loading.
601		 */
602		setLoadingState: function(loading) {
603			if (!this.button) {
604				return;
605			}
606
607			const buttonText = this.button.querySelector('.button-text');
608			const buttonLoading = this.button.querySelector('.button-loading');
609
610			if (loading) {
611				this.button.setAttribute('aria-busy', 'true');
612				this.button.disabled = true;
613				if (buttonText) buttonText.style.display = 'none';
614				if (buttonLoading) buttonLoading.style.display = 'inline';
615			} else {
616				this.button.setAttribute('aria-busy', 'false');
617				this.button.disabled = false;
618				if (buttonText) buttonText.style.display = 'inline';
619				if (buttonLoading) buttonLoading.style.display = 'none';
620			}
621		},
622
623		/**
624		 * Update button state based on available pages.
625		 */
626		updateButtonState: function() {
627			if (!this.button || !this.hasMore()) {
628				if (this.button) {
629					this.button.style.display = 'none';
630				}
631			}
632		},
633
634		/**
635		 * Show error message.
636		 *
637		 * @param {string} message Error message.
638		 */
639		showError: function(message) {
640			if (this.container) {
641				// Remove existing error message.
642				const existingError = this.container.querySelector('.bm-load-more-error');
643				if (existingError) {
644					existingError.remove();
645				}
646
647				const errorMsg = document.createElement('p');
648				errorMsg.className = 'bm-load-more-error';
649				errorMsg.textContent = message;
650				errorMsg.setAttribute('role', 'alert');
651				errorMsg.setAttribute('aria-live', 'assertive');
652				this.container.appendChild(errorMsg);
653
654				// Remove error message after 5 seconds.
655				setTimeout(() => {
656					if (errorMsg.parentNode) {
657						errorMsg.remove();
658					}
659				}, 5000);
660			}
661		},
662
663		/**
664		 * Announce to screen readers.
665		 *
666		 * @param {string} message Message to announce.
667		 */
668		announceToScreenReader: function(message) {
669			// Create or get live region.
670			let liveRegion = document.getElementById('bm-load-more-announce');
671			if (!liveRegion) {
672				liveRegion = document.createElement('div');
673				liveRegion.id = 'bm-load-more-announce';
674				liveRegion.setAttribute('role', 'status');
675				liveRegion.setAttribute('aria-live', 'polite');
676				liveRegion.setAttribute('aria-atomic', 'true');
677				liveRegion.className = 'screen-reader-text';
678				document.body.appendChild(liveRegion);
679			}
680
681			liveRegion.textContent = message;
682
683			// Clear after announcement.
684			setTimeout(() => {
685				liveRegion.textContent = '';
686			}, 1000);
687		},
688	};
689
690	// Initialize when DOM is ready.
691	if (document.readyState === 'loading') {
692		document.addEventListener('DOMContentLoaded', function() {
693			LoadMore.init();
694		});
695	} else {
696		LoadMore.init();
697	}
698})();
699

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.