PageSourceSearch

https://commercialobserver.com/wp-content/themes/observer-unchaine…/unchained-rest-api.js?m=1768929992g

js commercialobserver.com collected 2026-09-24 15:25:22 UTC 13,135 bytes, 419 lines download raw bytes

1/**
2 * REST API Load More on Click - reads config from data attributes
3 * Uses only WordPress built-in REST endpoints 
4 * OMR-193
5 */
6
7class PureRestLoadMore {
8	constructor(containerElement) {
9		this.debug = window.loadMoreGlobals.debug || false;
10
11		this.container = containerElement;
12		this.settings = this.parseDataAttributes(containerElement);
13		
14		this.allFetchedPosts = [];
15		this.filteredPosts = [];
16		this.currentlyShown = 0;
17		this.currentPage = 1;
18		this.totalPosts = 0;
19		this.loading = false;
20		this.allPagesFetched = false;
21		
22		this.serverRenderedCount = 0; // <-- FIX 1: Add property to track server-rendered count
23		
24		this.init();
25	}
26	
27	parseDataAttributes(element) {
28		const dataset = element.dataset;
29		
30		return {
31			postTypes: JSON.parse(dataset.postTypes || '["post"]'),
32			taxonomy: dataset.taxonomy || null,
33			termId: dataset.termId ? parseInt(dataset.termId) : null,
34			author: dataset.author ? parseInt(dataset.author) : null,
35			initialCount: parseInt(dataset.initialCount || 17),
36			loadMoreCount: parseInt(dataset.loadMoreCount || 12),
37			perPage: parseInt(dataset.perPage || 50),
38			excludeTaxonomies: JSON.parse(dataset.excludeTaxonomies || '{}'),
39			excludePosts: JSON.parse(dataset.excludePosts || '[]'),
40			showPostType: dataset.showPostType === 'true',
41			containerSelector: '.rest-posts-container',
42			buttonSelector: '.load-more-btn',
43			spinnerSelector: '.load-more-spinner',
44			endMessageSelector: '.load-more-end'
45		};
46	}
47	
48	async init() {
49		if(this.debug) console.log('Pure REST API Load More initialized:', this.settings);
50		if(this.debug) console.log('Load more count setting:', this.settings.loadMoreCount);
51		// Check if posts are already server-rendered
52		const existingPosts = this.container.querySelectorAll('.rest-item');
53		
54		if (existingPosts.length > 0) {
55			if(this.debug) console.log(`Found ${existingPosts.length} server-rendered posts`);
56
57			// FIX 2a: Set the baseline count
58			this.serverRenderedCount = existingPosts.length;
59			
60			if (this.settings.excludePosts.length > 0) {
61				// We are on a "complex" page (market, single) that uses exclude_posts.
62				// The API list will already be offset, so the JS should start its
63				// slice index from 0.
64				if(this.debug) console.log('Exclude mode: setting currentlyShown to 0');
65				this.currentlyShown = 0; // <-- KEEP THIS LINE (REQUIRED FOR FIRST SLICE INDEX)
66			} else {
67				// We are on a "simple" archive (category, author).
68				// The API list is not offset, so the JS must use the
69				// server post count (e.g., 17) as its offset.
70				if(this.debug) console.log('Simple mode: setting currentlyShown to server count');
71				this.currentlyShown = existingPosts.length;
72			}
73			
74			// Set current page to 0. The first "load more" click will
75			// increment this to 1 and fetch the first page of results.
76			this.currentPage = 0;
77			
78			// Set up for load more without initial fetch
79			this.updateLoadMoreButton();
80			this.bindEvents();
81			
82			// Show the load more button since we have server posts
83			this.showButton();
84		} else {
85			if(this.debug) console.log('No server posts found, using JavaScript-only mode');
86			// Original JavaScript-only approach
87			await this.fetchInitialData();
88			this.filterPosts();
89			await this.ensureEnoughFilteredPosts(this.settings.initialCount);
90			this.showInitialPosts();
91			this.bindEvents();
92		}
93	}
94	
95	buildApiUrl(page = 1) {
96		const params = new URLSearchParams({
97			post_type: this.settings.postTypes,
98			per_page: this.settings.perPage,
99			page: page,
100			orderby: 'date',
101			order: 'desc',
102		});
103		
104		if (this.settings.taxonomy && this.settings.termId) {
105			params.append('taxonomy', this.settings.taxonomy);
106			params.append('term_id', this.settings.termId);
107		}
108		if(this.settings.author) {
109			params.append('coauthors[]', this.settings.author);
110		}
111
112		if (Object.keys(this.settings.excludeTaxonomies).length > 0) {
113			// Encode it as a JSON string to be parsed by PHP
114			params.append('exclude_taxonomies', JSON.stringify(this.settings.excludeTaxonomies));
115		}
116
117		if (this.settings.excludePosts && this.settings.excludePosts.length > 0) {
118			// Pass as JSON string, similar to exclude_taxonomies
119			params.append('exclude_posts', JSON.stringify(this.settings.excludePosts));
120		}
121
122		return `${window.loadMoreGlobals.restUrl}multi-posts?${params.toString()}`;
123	}
124	
125	async fetchPage(page) {
126		if (this.allPagesFetched) {
127			return { success: false, posts: [] };
128		}
129		
130		if(this.debug) console.log(`Making API call for page ${page}`);
131		
132		try {
133			const url = this.buildApiUrl(page);
134			if(this.debug) console.log(`API URL: ${url}`);
135			
136			const response = await fetch(url);
137			
138			if (!response.ok) {
139				if (response.status === 400) {
140					if(this.debug) console.log('Page not found, marking all pages as fetched');
141					this.allPagesFetched = true;
142					return { success: false, posts: [] };
143				}
144				throw new Error(`HTTP ${response.status}: ${response.statusText}`);
145			}
146			
147			const data = await response.json();
148			
149			this.totalPosts = data.total;
150			this.allPagesFetched = !data.has_more || data.posts.length === 0;
151			
152			if(this.debug) console.log(`API Response:`, {
153				postsReturned: data.posts.length,
154				total: data.total,
155				hasMore: data.has_more,
156				allPagesFetched: this.allPagesFetched
157			});
158			
159			return { 
160				success: true, 
161				posts: data.posts,
162				hasMore: data.has_more
163			};
164			
165		} catch (error) {
166			console.error(`Failed to fetch page ${page}:`, error);
167			return { success: false, posts: [] };
168		}
169	}
170	
171	async fetchInitialData() {
172		const result = await this.fetchPage(1);
173		if (result.success) {
174			this.allFetchedPosts.push(...result.posts);
175			this.currentPage = 1;
176		}
177	}
178	
179	async fetchMoreData() {
180		if (this.allPagesFetched) {
181			if(this.debug) console.log('All pages already fetched');
182			return false;
183		}
184		
185		this.currentPage++;
186		if(this.debug) console.log(`Fetching page ${this.currentPage}`);
187		
188		const result = await this.fetchPage(this.currentPage);
189		
190		if (result.success && result.posts.length > 0) {
191			if(this.debug) console.log(`Successfully fetched ${result.posts.length} posts from page ${this.currentPage}`);
192			this.allFetchedPosts.push(...result.posts);
193			return true;
194		}
195		
196		if(this.debug) console.log(`No posts returned from page ${this.currentPage}`);
197		return false;
198	}
199	
200	filterPosts() {
201		if (Object.keys(this.settings.excludeTaxonomies).length === 0) {
202			this.filteredPosts = [...this.allFetchedPosts];
203			return;
204		}
205		
206		const beforeCount = this.filteredPosts.length;
207		
208		this.filteredPosts = this.allFetchedPosts.filter(post => {
209			for (const [taxonomy, excludedTerms] of Object.entries(this.settings.excludeTaxonomies)) {
210				const postTerms = post.custom_taxonomies[taxonomy] || [];
211				
212				if (excludedTerms.some(excludedTerm => postTerms.includes(excludedTerm))) {
213					return false;
214				}
215			}
216			return true;
217		});
218		
219		const newFiltered = this.filteredPosts.length - beforeCount;
220		if (newFiltered > 0) {
221			if(this.debug) console.log(`Filtered ${newFiltered} posts. Available: ${this.filteredPosts.length}/${this.allFetchedPosts.length}`);
222		}
223	}
224	
225	async ensureEnoughFilteredPosts(needed) {
226		let attempts = 0;
227		const maxAttempts = 20;
228		
229		if(this.debug) console.log(`ensureEnoughFilteredPosts: need ${needed}, have ${this.filteredPosts.length}`);
230		
231		while (
232			this.filteredPosts.length < needed && 
233			!this.allPagesFetched &&
234			attempts < maxAttempts
235		) {
236			if(this.debug) console.log(`Attempt ${attempts + 1}: Fetching more data...`);
237			const success = await this.fetchMoreData();
238			if (!success) {
239				if(this.debug) console.log('Failed to fetch more data, breaking');
240				break;
241			}
242			
243			this.filterPosts();
244			attempts++;
245			
246			if(this.debug) console.log(`After attempt ${attempts}: Have ${this.filteredPosts.length} filtered posts`);
247		}
248		
249		if (attempts === 0) {
250			if(this.debug) console.log('No new API calls needed - using cached posts');
251		}
252	}
253	
254	showInitialPosts() {
255		const initialPosts = this.filteredPosts.slice(0, this.settings.initialCount);
256		this.renderPosts(initialPosts);
257		this.currentlyShown = initialPosts.length;
258		this.updateLoadMoreButton();
259		if(this.debug) console.log(`Showing initial ${initialPosts.length} posts`);
260	}
261	
262	async loadMore() {
263		if (this.loading) return;
264		
265		if(this.debug) console.log('Load More clicked - Current state:', {
266			currentlyShown: this.currentlyShown,
267			filteredPostsAvailable: this.filteredPosts.length,
268			loadMoreCount: this.settings.loadMoreCount
269		});
270		
271		this.loading = true;
272		this.showSpinner();
273		this.hideButton();
274		
275		// FIX 3a: Replace unreliable flag with a robust check for first click in exclude mode
276		const didExclude = (this.settings.excludePosts.length > 0);
277		const isFirstLoadMoreClick = (this.currentlyShown === 0 && didExclude);
278
279		// Check if we have enough cached posts to show
280		const postsNeeded = this.currentlyShown + this.settings.loadMoreCount;
281		
282		// Fetch more data if we don't have enough cached posts
283		if (this.filteredPosts.length < postsNeeded && !this.allPagesFetched) {
284			await this.ensureEnoughFilteredPosts(postsNeeded);
285		}
286		
287		// Get the next batch of posts to show (just the loadMoreCount amount)
288		
289		// SLICING FIX: If it's the first click in exclude mode, start at 0. Otherwise, use the running total.
290		const startIndex = isFirstLoadMoreClick ? 0 : this.currentlyShown;
291		const endIndex = startIndex + this.settings.loadMoreCount;
292		const postsToShow = this.filteredPosts.slice(startIndex, endIndex);
293		
294		if(this.debug) console.log(`Showing posts ${startIndex} to ${endIndex - 1} (${postsToShow.length} posts)`);
295		
296		setTimeout(() => {
297			if (postsToShow.length > 0) {
298				this.renderPosts(postsToShow, true); // append = true
299
300				// FIX 3b: CORRECT COUNTER UPDATE LOGIC (CRITICAL STEP)
301				// The counter must track the index within the internal array, NOT the total visible count.
302				this.currentlyShown = this.currentlyShown + postsToShow.length; 
303				
304				if(this.debug) console.log(`Loaded ${postsToShow.length} more. Total shown: ${this.currentlyShown}`);
305				
306				// Check if we can load more
307				const canLoadMore = this.currentlyShown < this.filteredPosts.length || !this.allPagesFetched;
308				
309				if (canLoadMore) {
310					this.showButton();
311				} else {
312					this.showEndMessage();
313				}
314			} else {
315				this.showEndMessage();
316			}
317			
318			this.hideSpinner();
319			this.loading = false;
320		}, 200);
321	}
322	
323	renderPosts(posts, append = false) {
324		const container = this.container.querySelector(this.settings.containerSelector);
325
326		if (!container) return;
327		
328		if (!append) {
329			container.innerHTML = '';
330		}
331		
332		posts.forEach(post => {
333			container.appendChild(this.createPostElement(post));
334		});
335	}
336	
337	createPostElement(post) {
338		const template = document.getElementById('post-card-template');
339		if (template) {
340			// Use the template system (Underscore.js or modern template literal)
341			const templateFn = _.template(template.innerHTML);
342			const html = templateFn(post);
343			
344			const tempDiv = document.createElement('div');
345			tempDiv.innerHTML = html;
346			return tempDiv.firstElementChild;
347		}
348	}
349
350	updateLoadMoreButton() {
351		const canLoadMore = this.currentlyShown < this.filteredPosts.length || !this.allPagesFetched;
352		
353		if (canLoadMore) {
354			this.showButton();
355			this.hideEndMessage();
356		} else {
357			this.hideButton();
358			this.showEndMessage();
359		}
360	}
361	
362	bindEvents() {
363		const button = this.container.querySelector(this.settings.buttonSelector);
364
365		if (button) {
366			button.addEventListener('click', (e) => {
367				e.preventDefault();
368				this.loadMore();
369			});
370		}
371	}
372	
373	showButton() { this.toggleElement(this.settings.buttonSelector, true); }
374	hideButton() { this.toggleElement(this.settings.buttonSelector, false); }
375	showSpinner() { this.toggleElement(this.settings.spinnerSelector, true); }
376	hideSpinner() { this.toggleElement(this.settings.spinnerSelector, false); }
377	showEndMessage() { 
378		this.toggleElement(this.settings.endMessageSelector, true); 
379		this.hideButton();
380	}
381	hideEndMessage() { this.toggleElement(this.settings.endMessageSelector, false); }
382	
383	toggleElement(selector, show) {
384		const el = this.container.querySelector(selector);
385		if (el) el.style.display = show ? 'block' : 'none';
386	}
387	
388	// Public methods for debugging
389	getStats() {
390		return {
391			totalPosts: this.totalPosts,
392			fetchedPosts: this.allFetchedPosts.length,
393			filteredPosts: this.filteredPosts.length,
394			currentlyShown: this.currentlyShown,
395			currentPage: this.currentPage,
396			allPagesFetched: this.allPagesFetched,
397			settings: this.settings
398		};
399	}
400}
401
402// Auto-initialize all load more containers
403document.addEventListener('DOMContentLoaded', function() {
404	const containers = document.querySelectorAll('.load-more-container');
405	
406	containers.forEach(container => {
407		new PureRestLoadMore(container);
408	});
409	
410	// Global debug helper
411	window.debugLoadMore = function() {
412		const containers = document.querySelectorAll('.load-more-container');
413		containers.forEach((container, index) => {
414			if (container.loadMoreInstance) {
415				if(this.debug) console.log(`Load More Instance ${index}:`, container.loadMoreInstance.getStats());
416			}
417		});
418	};
419});

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.