PageSourceSearch

https://attractioninsight.com/wp-content/plugins/attraction-insigh…720'%20defer%20onload=''%20defer%20onload='

js attractioninsight.com collected 2026-10-02 22:35:46 UTC 13,346 bytes, 492 lines download raw bytes

1/**
2 * Post Listings Topics Script
3 * 
4 * For .ai-grand post listings with a topics menu, replaces the
5 * browser's default dropdown with a custom one and loads posts
6 * via AJAX based on the user's selected topic
7 */
8
9(function () {
10	// Safely parse JSON arrays from data attributes
11	function parseJsonArray(value) {
12		try {
13			const parsed = JSON.parse(value || '[]');
14			return Array.isArray(parsed) ? parsed : [];
15		} catch (error) {
16			return [];
17		}
18	}
19
20	// Ensure URLs are safe and valid before using them
21	function safeUrl(url) {
22		if (!url) return '';
23
24		try {
25			const parsed = new URL(url, window.location.origin);
26
27			if (parsed.protocol === 'http:' || parsed.protocol === 'https:') {
28				return parsed.href;
29			}
30		} catch (error) {
31			// Allow relative URLs starting with "/"
32			if (url.startsWith('/')) {
33				return url;
34			}
35		}
36
37		return '';
38	}
39
40	// Generate a unique cache key based on menu item filters
41	function getTopicCacheKey(menuItem) {
42		return JSON.stringify({
43			categories: parseJsonArray(menuItem.dataset.categories),
44			tags: parseJsonArray(menuItem.dataset.tags),
45			excludeCategories: parseJsonArray(menuItem.dataset.excludeCategories),
46			url: menuItem.dataset.url || '',
47			linkText: menuItem.dataset.linkText || ''
48		});
49	}
50
51	// Adjust dropdown alignment if it overflows the viewport
52	function positionMoreDropdown(moreWrap, dropdown) {
53		if (!moreWrap || !dropdown) return;
54
55		const gutter = 10;
56		const rect = dropdown.getBoundingClientRect();
57
58		// If dropdown is too close to left edge, force left alignment
59		if (rect.left < gutter) {
60			moreWrap.classList.add('align-left');
61		}
62	}
63
64	function createPostImageLink(post) {
65		if (!post.image_src) {
66			return null;
67		}
68
69		const safePermalink = safeUrl(post.permalink);
70		const safeImageSrc = safeUrl(post.image_src);
71
72		if (!safePermalink || !safeImageSrc) {
73			return null;
74		}
75
76		const link = document.createElement('a');
77		link.href = safePermalink;
78
79		const image = document.createElement('img');
80		image.src = safeImageSrc;
81		image.alt = post.image_alt || post.title || '';
82		image.loading = 'lazy';
83
84		link.appendChild(image);
85
86		return link;
87	}
88
89	// Create post title element
90	function createPostTitle(post) {
91		const heading = document.createElement('h3');
92		heading.className = 'item-title';
93
94		const link = document.createElement('a');
95		link.href = safeUrl(post.permalink);
96		link.textContent = post.title || '(Untitled)';
97
98		heading.appendChild(link);
99
100		return heading;
101	}
102
103	// Create post date element
104	function createPostDate(post) {
105		if (!post.date_display || !post.date_iso) {
106			return null;
107		}
108
109		const time = document.createElement('time');
110		time.dateTime = post.date_iso;
111		time.textContent = post.date_display;
112
113		return time;
114	}
115
116	// Spotlight Feature layout post
117	function createFeaturePost(post) {
118		const item = document.createElement('div');
119		item.className = 'post-item feature';
120
121		const imageLink = createPostImageLink(post);
122		if (imageLink) {
123			item.appendChild(imageLink);
124		}
125
126		const content = document.createElement('div');
127		content.className = 'content-section';
128
129		content.appendChild(createPostTitle(post));
130
131		const date = createPostDate(post);
132		if (date) {
133			content.appendChild(date);
134		}
135
136		item.appendChild(content);
137
138		return item;
139	}
140
141	// Spotlight Stack layout post
142	function createStackPost(post) {
143		const item = document.createElement('div');
144		item.className = 'post-item stack';
145
146		const imageSection = document.createElement('div');
147		imageSection.className = 'image-section';
148
149		const imageLink = createPostImageLink(post);
150		if (imageLink) {
151			imageSection.appendChild(imageLink);
152		}
153
154		item.appendChild(imageSection);
155
156		const content = document.createElement('div');
157		content.className = 'content-section';
158
159		content.appendChild(createPostTitle(post));
160
161		const date = createPostDate(post);
162		if (date) {
163			content.appendChild(date);
164		}
165
166		item.appendChild(content);
167
168		return item;
169	}
170
171	// Default layout post
172	function createStandardPost(post) {
173		const item = document.createElement('div');
174		item.className = 'post-item';
175
176		const imageSection = document.createElement('div');
177		imageSection.className = 'image-section';
178
179		const imageLink = createPostImageLink(post);
180		if (imageLink) {
181			imageSection.appendChild(imageLink);
182		}
183
184		item.appendChild(imageSection);
185
186		const content = document.createElement('div');
187		content.className = 'content-section';
188
189		content.appendChild(createPostTitle(post));
190
191		const date = createPostDate(post);
192		if (date) {
193			content.appendChild(date);
194		}
195
196		item.appendChild(content);
197
198		return item;
199	}
200
201	// Render posts into container
202	function renderPostsShell(postsShell, posts) {
203		if (!postsShell) return;
204
205		postsShell.replaceChildren();
206
207		// Show empty state if no posts
208		if (!Array.isArray(posts) || !posts.length) {
209			const empty = document.createElement('p');
210			empty.textContent = 'No posts found.';
211			postsShell.appendChild(empty);
212			return;
213		}
214
215		const blockContent = document.createElement('div');
216		blockContent.className = 'block-content';
217
218		posts.forEach(function (post) {
219			if (!post) return;
220
221			// Choose layout type
222			if (post.feature) {
223				blockContent.appendChild(createFeaturePost(post));
224			} else if (post.stack) {
225				blockContent.appendChild(createStackPost(post));
226			} else {
227				blockContent.appendChild(createStandardPost(post));
228			}
229		});
230
231		postsShell.appendChild(blockContent);
232	}
233
234	// Main click handler (event delegation)
235	document.addEventListener('click', function (event) {
236
237		// Handle "More" toggle click (mobile only)
238		const moreToggle = event.target.closest('.ai-grand-instance .menu-more-toggle');
239
240		if (moreToggle) {
241			// Skip if hover-capable device (desktop)
242			if (window.matchMedia('(hover: hover) and (pointer: fine)').matches) return;
243
244			const moreWrap = moreToggle.closest('.menu-more');
245			if (!moreWrap) return;
246
247			const isOpen = moreWrap.classList.contains('is-open');
248
249			if (isOpen) {
250				closeMoreMenu(moreWrap);
251			} else {
252				openMoreMenu(moreWrap);
253			}
254
255			return;
256		}
257
258		// Handle menu item click
259		const menuItem = event.target.closest('.ai-grand-instance .menu-item:not(.menu-more-toggle)');
260
261		// Click outside menu → close dropdowns
262		if (!menuItem) {
263			const clickedInsideMore = event.target.closest('.ai-grand-instance .menu-more');
264
265			if (!clickedInsideMore) {
266				document.querySelectorAll('.ai-grand-instance .menu-more').forEach(function (menu) {
267					menu.classList.remove('is-open');
268
269					const toggle = menu.querySelector('.menu-more-toggle');
270					const panel = menu.querySelector('.menu-more-dropdown');
271
272					if (toggle) {
273						toggle.classList.remove('active');
274						toggle.setAttribute('aria-expanded', 'false');
275					}
276
277					if (panel) {
278						panel.hidden = true;
279					}
280				});
281			}
282
283			return;
284		}
285
286		const instance = menuItem.closest('.ai-grand-instance');
287		if (!instance) return;
288
289		// Prevent duplicate requests
290		if (instance.classList.contains('is-loading')) return;
291		if (menuItem.classList.contains('active')) return;
292
293		// Initialize cache
294		if (!instance._topicCache) {
295			instance._topicCache = {};
296		}
297
298		const allMenuItems = instance.querySelectorAll('.menu-item');
299		const postsShell = instance.querySelector('.posts-shell');
300		const morePostsLink = instance.querySelector('.action .link');
301
302		// Update active state
303		allMenuItems.forEach(function (item) {
304			item.classList.remove('active');
305		});
306
307		menuItem.classList.add('active');
308
309		// Close all "more" menus
310		instance.querySelectorAll('.menu-more').forEach(function (menu) {
311			menu.classList.remove('is-open');
312
313			const toggle = menu.querySelector('.menu-more-toggle');
314			const panel = menu.querySelector('.menu-more-dropdown');
315
316			if (toggle) {
317				toggle.classList.remove('active');
318				toggle.setAttribute('aria-expanded', 'false');
319			}
320
321			if (panel) {
322				panel.hidden = true;
323			}
324		});
325
326		// Update "See more posts" link
327		if (morePostsLink) {
328			morePostsLink.setAttribute('href', safeUrl(menuItem.dataset.url));
329			morePostsLink.textContent = menuItem.dataset.linkText || 'See More Posts';
330		}
331
332		const cacheKey = getTopicCacheKey(menuItem);
333
334		// Use cached posts if available
335		if (instance._topicCache[cacheKey] && postsShell) {
336			renderPostsShell(postsShell, instance._topicCache[cacheKey]);
337			return;
338		}
339
340		// Build AJAX request
341		const formData = new FormData();
342		formData.append('action', 'ai_spotlight_posts_filter');
343		formData.append('nonce', instance.dataset.nonce);
344		formData.append('total', instance.dataset.total);
345		formData.append('orderby', instance.dataset.orderby);
346		formData.append('order', instance.dataset.order);
347		formData.append('show_date', instance.dataset.showDate);
348		formData.append('layout_class', instance.dataset.layoutClass || 'feature');
349
350		const categories = parseJsonArray(menuItem.dataset.categories);
351		categories.forEach(function (category) {
352			formData.append('categories[]', category);
353		});
354
355		const tags = parseJsonArray(menuItem.dataset.tags);
356		tags.forEach(function (tag) {
357			formData.append('tags[]', tag);
358		});
359
360		const excludeCategories = parseJsonArray(menuItem.dataset.excludeCategories);
361		excludeCategories.forEach(function (category) {
362			formData.append('exclude_categories[]', category);
363		});
364
365		// Cancel previous request if needed
366		if (instance._topicRequestController) {
367			instance._topicRequestController.abort();
368		}
369
370		instance._topicRequestController = new AbortController();
371
372		instance.classList.add('is-loading');
373
374		// Fetch posts
375		fetch(instance.dataset.ajaxUrl, {
376			method: 'POST',
377			body: formData,
378			credentials: 'same-origin',
379			signal: instance._topicRequestController.signal
380		})
381			.then(function (response) {
382				if (!response.ok) {
383					throw new Error('Request failed with status ' + response.status);
384				}
385
386				return response.json();
387			})
388			.then(function (response) {
389
390				if (response && response.success && response.data && Array.isArray(response.data.posts) && postsShell) {
391					instance._topicCache[cacheKey] = response.data.posts;
392					renderPostsShell(postsShell, response.data.posts);
393				} else {
394					console.error('AJAX unexpected response shape:', response);
395				}
396
397				instance.classList.remove('is-loading');
398			})
399			.catch(function (error) {
400				if (error.name === 'AbortError') {
401					instance.classList.remove('is-loading');
402					return;
403				}
404
405				console.error('Posts request failed:', error);
406				instance.classList.remove('is-loading');
407			});
408	});
409
410	// Open "More" dropdown
411	function openMoreMenu(moreWrap) {
412		const toggle = moreWrap.querySelector('.menu-more-toggle');
413		const dropdown = moreWrap.querySelector('.menu-more-dropdown');
414		const instance = moreWrap.closest('.ai-grand-instance');
415
416		if (!toggle || !dropdown || !instance) return;
417
418		// Close other menus first
419		instance.querySelectorAll('.menu-more').forEach(function (menu) {
420			menu.classList.remove('is-open');
421
422			const otherToggle = menu.querySelector('.menu-more-toggle');
423			const otherDropdown = menu.querySelector('.menu-more-dropdown');
424
425			if (otherToggle) {
426				otherToggle.classList.remove('active');
427				otherToggle.setAttribute('aria-expanded', 'false');
428			}
429
430			if (otherDropdown) {
431				otherDropdown.hidden = true;
432			}
433
434			// reset alignment state for other menus
435			menu.classList.remove('align-left');
436		});
437
438		// reset alignment before measuring
439		moreWrap.classList.remove('align-left');
440
441		moreWrap.classList.add('is-open');
442		toggle.classList.add('active');
443		toggle.setAttribute('aria-expanded', 'true');
444		dropdown.hidden = false;
445
446		requestAnimationFrame(function () {
447			positionMoreDropdown(moreWrap, dropdown);
448		});
449	}
450
451	// Close "More" dropdown
452	function closeMoreMenu(moreWrap) {
453		const toggle = moreWrap.querySelector('.menu-more-toggle');
454		const dropdown = moreWrap.querySelector('.menu-more-dropdown');
455
456		if (!toggle || !dropdown) return;
457
458		moreWrap.classList.remove('is-open');
459		toggle.classList.remove('active');
460		toggle.setAttribute('aria-expanded', 'false');
461		dropdown.hidden = true;
462	}
463
464	// Desktop hover behavior
465	if (window.matchMedia('(hover: hover) and (pointer: fine)').matches) {
466		document.querySelectorAll('.ai-grand-instance .menu-more').forEach(function (moreWrap) {
467			let leaveTimer;
468
469			moreWrap.addEventListener('mouseenter', function () {
470				clearTimeout(leaveTimer);
471				openMoreMenu(moreWrap);
472			});
473
474			moreWrap.addEventListener('mouseleave', function () {
475				leaveTimer = setTimeout(function () {
476					closeMoreMenu(moreWrap);
477				}, 120);
478			});
479		});
480	}
481
482	// Reposition dropdowns on resize
483	window.addEventListener('resize', function () {
484		document.querySelectorAll('.ai-grand-instance .menu-more.is-open').forEach(function (moreWrap) {
485			const dropdown = moreWrap.querySelector('.menu-more-dropdown');
486
487			if (dropdown) {
488				positionMoreDropdown(moreWrap, dropdown);
489			}
490		});
491	});
492})();

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.