PageSourceSearch

https://nam.org/wp-content/themes/wdg-nam/template-parts/blocks/ar…/archive-listing.js?ver=1790237639

js nam.org collected 2026-09-24 08:33:02 UTC 19,548 bytes, 615 lines download raw bytes

1(function () {
2	'use strict';
3
4	document.addEventListener('DOMContentLoaded', function () {
5		var blocks = document.querySelectorAll('.archive-listing-block');
6		if (!blocks.length) return;
7
8		blocks.forEach(function (block) {
9			initBlock(block);
10		});
11
12		bindOriginMemory();
13		applyCardTitleTooltips(document);
14	});
15
16	/* Card titles are line-clamped; when one actually truncates, surface the
17	 * full headline as a native hover tooltip on its link. Re-run after every
18	 * AJAX re-render. */
19	function applyCardTitleTooltips(scope) {
20		(scope || document).querySelectorAll('.archive-card .post__title').forEach(function (heading) {
21			var link = heading.querySelector('.post__link') || heading;
22			if (heading.scrollHeight > heading.clientHeight + 2) {
23				link.setAttribute('title', (link.textContent || '').trim());
24			} else {
25				link.removeAttribute('title');
26			}
27		});
28	}
29
30	/* ── Origin memory ─────────────────────────────────────────────────
31	 * Every card/aside navigation off the listing records the exact
32	 * listing URL (live search/filter/page query included) plus the page
33	 * title in sessionStorage; assets/js/single-article.js reads it on the
34	 * story page and upgrades the breadcrumb so "back" returns to the
35	 * precise listing state. Plain navigation — never preventDefault.
36	 */
37	function bindOriginMemory() {
38		var ORIGIN_KEY = 'nam_alb_origin';
39
40		function listingLabel() {
41			var h1 = document.querySelector('h1.entry-title') || document.querySelector('.entry-header .entry-title');
42			var label = h1 ? h1.textContent.trim() : '';
43			if (!label) {
44				label = (document.title || '').split(/\s[-|\u2013\u2014]\s/)[0].trim();
45			}
46			return label || 'the archive';
47		}
48
49		document.addEventListener('click', function (e) {
50			var link = e.target.closest('a');
51			if (!link || !link.closest('.archive-listing-block')) return;
52
53			var card = link.closest('article.archive-card');
54			var isCardLink = !!card && (link.classList.contains('post__link') || link.classList.contains('archive-card__media-link'));
55			var isAsideLink = link.classList.contains('aside__title');
56			if (!isCardLink && !isAsideLink) return;
57
58			try {
59				sessionStorage.setItem(ORIGIN_KEY, JSON.stringify({
60					url: window.location.href,
61					label: listingLabel(),
62					ts: Date.now()
63				}));
64			} catch (err) { /* storage unavailable */ }
65		});
66	}
67
68	function initBlock(block) {
69		var configAttr = block.getAttribute('data-alb-config');
70		if (!configAttr) return;
71
72		var config;
73		try {
74			config = JSON.parse(configAttr);
75		} catch (e) {
76			return;
77		}
78
79		var state = {
80			postTypes: [],
81			taxonomies: {},
82			search: '',
83			paged: 1
84		};
85
86		var resultsEl    = block.querySelector('.archive-listing__results');
87		var paginationEl = block.querySelector('.archive-listing__pagination');
88		var filtersEl    = block.querySelector('.archive-listing__filters');
89		var controlsEl   = block.querySelector('.archive-listing__controls');
90		var searchInput  = block.querySelector('[data-archive-search]');
91		// The columns' list (grid + pagination) — NOT the lead wrapper, which
92		// also carries .archive__list for the shared card styling.
93		var listEl       = block.querySelector('.archive-listing__columns .archive__list');
94		var basePath     = window.location.pathname.replace(/\/page\/\d+\/?$/, '/');
95
96		initStateFromUrl();
97
98		// Normalize /page/N/ URLs to query-string format so pushUrl() works
99		// consistently and the aside panel slug is derived correctly.
100		if (/\/page\/\d+\/?$/.test(window.location.pathname)) {
101			var normalizeParams = buildUrlParams();
102			var normalizeQs = normalizeParams.toString();
103			var cleanUrl = basePath + (normalizeQs ? '?' + normalizeQs : '');
104			window.history.replaceState(null, '', cleanUrl);
105
106			// Fix the browser tab title if it still shows the 404 title.
107			if (document.title.toLowerCase().indexOf('not found') !== -1) {
108				var pageHeading = document.querySelector('.entry-title');
109				if (pageHeading) {
110					document.title = document.title.replace(/page not found/i, pageHeading.textContent.trim());
111				}
112			}
113		}
114
115		syncSearchUI();
116		bindSearch();
117		bindPillClicks();
118		bindShowMore();
119		bindDropdownFilters();
120		bindPaginationClicks();
121		window.addEventListener('popstate', function () {
122			initStateFromUrl();
123			syncSearchUI();
124			syncPillUI();
125			syncDropdownUI();
126			fetchResults();
127		});
128
129		function initStateFromUrl() {
130			var params = new URLSearchParams(window.location.search);
131
132			state.paged = parseInt(params.get('paged'), 10) || 1;
133			if (state.paged <= 1) {
134				var pathMatch = window.location.pathname.match(/\/page\/(\d+)\/?$/);
135				if (pathMatch) {
136					state.paged = parseInt(pathMatch[1], 10) || 1;
137				}
138			}
139			state.search = params.get('alb_search') || '';
140
141			state.postTypes = [];
142			if (params.has('alb_post_type')) {
143				state.postTypes = params.get('alb_post_type').split(',').filter(Boolean);
144			}
145
146			state.taxonomies = {};
147			if (config.filterTaxonomies) {
148				config.filterTaxonomies.forEach(function (tax) {
149					var key = 'alb_' + tax;
150					if (params.has(key)) {
151						state.taxonomies[tax] = params.get(key).split(',').filter(Boolean);
152					}
153				});
154			}
155		}
156
157		function buildUrlParams() {
158			var params = new URLSearchParams();
159
160			if (state.search) {
161				params.set('alb_search', state.search);
162			}
163
164			if (state.postTypes.length > 0) {
165				params.set('alb_post_type', state.postTypes.join(','));
166			}
167
168			Object.keys(state.taxonomies).forEach(function (tax) {
169				if (state.taxonomies[tax].length > 0) {
170					params.set('alb_' + tax, state.taxonomies[tax].join(','));
171				}
172			});
173
174			if (state.paged > 1) {
175				params.set('paged', state.paged);
176			}
177
178			return params;
179		}
180
181		function debounce(fn, delay) {
182			var timer;
183			return function () {
184				var args = arguments;
185				clearTimeout(timer);
186				timer = setTimeout(function () {
187					fn.apply(null, args);
188				}, delay);
189			};
190		}
191
192		function pushUrl() {
193			var params = buildUrlParams();
194			var qs = params.toString();
195			var url = basePath + (qs ? '?' + qs : '');
196			window.history.pushState(null, '', url);
197		}
198
199		function syncSearchUI() {
200			if (!searchInput) return;
201			searchInput.value = state.search;
202		}
203
204		function runSearch(value) {
205			state.search = value.trim();
206			state.paged = 1;
207			pushUrl();
208			fetchResults();
209		}
210
211		function bindSearch() {
212			if (!controlsEl || !searchInput) return;
213
214			var debouncedSearch = debounce(function () {
215				runSearch(searchInput.value);
216			}, 350);
217
218			controlsEl.addEventListener('submit', function (e) {
219				e.preventDefault();
220				runSearch(searchInput.value);
221			});
222
223			searchInput.addEventListener('input', debouncedSearch);
224		}
225
226		function syncPillUI() {
227			if (!filtersEl) return;
228
229			var ptPills = filtersEl.querySelectorAll('[data-filter-type="post_type"]');
230			ptPills.forEach(function (btn) {
231				var item = btn.closest('.archive-listing__facet-item');
232				if (!item) return;
233				var val = btn.getAttribute('data-value');
234				item.classList.toggle('is-active', state.postTypes.indexOf(val) !== -1);
235			});
236
237			var taxPills = filtersEl.querySelectorAll('button[data-filter-type="taxonomy"]');
238			taxPills.forEach(function (btn) {
239				var item = btn.closest('.archive-listing__facet-item');
240				if (!item) return;
241				var tax = btn.getAttribute('data-taxonomy');
242				var val = btn.getAttribute('data-value');
243				var active = state.taxonomies[tax] && state.taxonomies[tax].indexOf(val) !== -1;
244				item.classList.toggle('is-active', active);
245			});
246		}
247
248		function bindPillClicks() {
249			if (!filtersEl) return;
250
251			filtersEl.addEventListener('click', function (e) {
252				var pill = e.target.closest('.archive-listing__facet-pill');
253				if (!pill) return;
254
255				var filterType = pill.getAttribute('data-filter-type');
256				var value      = pill.getAttribute('data-value');
257
258				if (filterType === 'post_type') {
259					var idx = state.postTypes.indexOf(value);
260					if (idx === -1) {
261						state.postTypes.push(value);
262					} else {
263						state.postTypes.splice(idx, 1);
264					}
265				} else if (filterType === 'taxonomy') {
266					var tax = pill.getAttribute('data-taxonomy');
267					if (!state.taxonomies[tax]) {
268						state.taxonomies[tax] = [];
269					}
270					var tidx = state.taxonomies[tax].indexOf(value);
271					if (tidx === -1) {
272						state.taxonomies[tax].push(value);
273					} else {
274						state.taxonomies[tax].splice(tidx, 1);
275					}
276					if (state.taxonomies[tax].length === 0) {
277						delete state.taxonomies[tax];
278					}
279				}
280
281				state.paged = 1;
282				syncPillUI();
283				pushUrl();
284				fetchResults();
285			});
286		}
287
288		function bindShowMore() {
289			if (!filtersEl) return;
290
291			filtersEl.addEventListener('click', function (e) {
292				var btn = e.target.closest('.archive-listing__show-more');
293				if (!btn) return;
294
295				var group = btn.closest('.archive-listing__facet-group');
296				if (!group) return;
297
298				var hidden = group.querySelectorAll('[data-overflow="true"]');
299				var isExpanded = btn.getAttribute('data-expanded') === 'true';
300
301				hidden.forEach(function (el) {
302					el.style.display = isExpanded ? 'none' : '';
303				});
304
305				btn.setAttribute('data-expanded', isExpanded ? 'false' : 'true');
306				btn.textContent = isExpanded ? 'Show more' : 'Show less';
307			});
308		}
309
310		function bindDropdownFilters() {
311			if (!filtersEl) return;
312
313			var groups = filtersEl.querySelectorAll('[data-facet-dropdown]');
314			groups.forEach(function (group) {
315				var tax = group.getAttribute('data-taxonomy');
316				var wrap = group.querySelector('.archive-listing__dropdown-wrap');
317				var searchInput = group.querySelector('.archive-listing__dropdown-search');
318				var listEl = group.querySelector('.archive-listing__dropdown-list');
319				var tagsEl = group.querySelector('.archive-listing__dropdown-tags');
320				if (!wrap || !searchInput || !listEl || !tagsEl) return;
321
322				var closeTimer = null;
323
324				searchInput.addEventListener('focus', function () {
325					clearTimeout(closeTimer);
326					wrap.classList.add('is-open');
327				});
328
329				searchInput.addEventListener('blur', function () {
330					closeTimer = setTimeout(function () {
331						wrap.classList.remove('is-open');
332					}, 200);
333				});
334
335				searchInput.addEventListener('input', function () {
336					var q = searchInput.value.toLowerCase();
337					var opts = listEl.querySelectorAll('.archive-listing__dropdown-option');
338					opts.forEach(function (opt) {
339						var label = opt.querySelector('.archive-listing__dropdown-option-label');
340						var text = label ? label.textContent.toLowerCase() : '';
341						opt.classList.toggle('is-hidden', q !== '' && text.indexOf(q) === -1);
342					});
343				});
344
345				listEl.addEventListener('change', function (e) {
346					var cb = e.target;
347					if (cb.type !== 'checkbox') return;
348
349					var value = cb.value;
350					var label = cb.getAttribute('data-label');
351					var opt = cb.closest('.archive-listing__dropdown-option');
352
353					if (!state.taxonomies[tax]) {
354						state.taxonomies[tax] = [];
355					}
356
357					if (cb.checked) {
358						if (state.taxonomies[tax].indexOf(value) === -1) {
359							state.taxonomies[tax].push(value);
360						}
361						if (opt) opt.classList.add('is-checked');
362						var tag = document.createElement('span');
363						tag.className = 'archive-listing__dropdown-tag';
364						tag.setAttribute('data-value', value);
365						var tagColor = cb.getAttribute('data-color');
366						if (tagColor) tag.style.setProperty('--alb-facet-color', tagColor);
367						tag.innerHTML = label + '<button type="button" class="archive-listing__dropdown-tag-remove" aria-label="Remove ' + label + '">&times;</button>';
368						tagsEl.appendChild(tag);
369					} else {
370						var idx = state.taxonomies[tax].indexOf(value);
371						if (idx !== -1) state.taxonomies[tax].splice(idx, 1);
372						if (state.taxonomies[tax].length === 0) delete state.taxonomies[tax];
373						if (opt) opt.classList.remove('is-checked');
374						var existing = tagsEl.querySelector('[data-value="' + value + '"]');
375						if (existing) existing.remove();
376					}
377
378					state.paged = 1;
379					syncPillUI();
380					pushUrl();
381					fetchResults();
382				});
383
384				tagsEl.addEventListener('click', function (e) {
385					var removeBtn = e.target.closest('.archive-listing__dropdown-tag-remove');
386					if (!removeBtn) return;
387
388					var tag = removeBtn.closest('.archive-listing__dropdown-tag');
389					if (!tag) return;
390
391					var value = tag.getAttribute('data-value');
392					tag.remove();
393
394					var cb = listEl.querySelector('input[type="checkbox"][value="' + value + '"]');
395					if (cb) {
396						cb.checked = false;
397						var opt = cb.closest('.archive-listing__dropdown-option');
398						if (opt) opt.classList.remove('is-checked');
399					}
400
401					if (state.taxonomies[tax]) {
402						var idx = state.taxonomies[tax].indexOf(value);
403						if (idx !== -1) state.taxonomies[tax].splice(idx, 1);
404						if (state.taxonomies[tax].length === 0) delete state.taxonomies[tax];
405					}
406
407					state.paged = 1;
408					syncPillUI();
409					pushUrl();
410					fetchResults();
411				});
412			});
413		}
414
415		function syncDropdownUI() {
416			if (!filtersEl) return;
417
418			var groups = filtersEl.querySelectorAll('[data-facet-dropdown]');
419			groups.forEach(function (group) {
420				var tax = group.getAttribute('data-taxonomy');
421				var listEl = group.querySelector('.archive-listing__dropdown-list');
422				var tagsEl = group.querySelector('.archive-listing__dropdown-tags');
423				if (!listEl || !tagsEl) return;
424
425				var activeTerms = state.taxonomies[tax] || [];
426
427				listEl.querySelectorAll('input[type="checkbox"]').forEach(function (cb) {
428					var isActive = activeTerms.indexOf(cb.value) !== -1;
429					cb.checked = isActive;
430					var opt = cb.closest('.archive-listing__dropdown-option');
431					if (opt) opt.classList.toggle('is-checked', isActive);
432				});
433
434				tagsEl.innerHTML = '';
435				activeTerms.forEach(function (val) {
436					var cb = listEl.querySelector('input[type="checkbox"][value="' + val + '"]');
437					var label = cb ? cb.getAttribute('data-label') : val;
438					var tag = document.createElement('span');
439					tag.className = 'archive-listing__dropdown-tag';
440					tag.setAttribute('data-value', val);
441					var tagColor = cb ? cb.getAttribute('data-color') : '';
442					if (tagColor) tag.style.setProperty('--alb-facet-color', tagColor);
443					tag.innerHTML = label + '<button type="button" class="archive-listing__dropdown-tag-remove" aria-label="Remove ' + label + '">&times;</button>';
444					tagsEl.appendChild(tag);
445				});
446			});
447		}
448
449		function bindPaginationClicks() {
450			block.addEventListener('click', function (e) {
451				var link = e.target.closest('.archive-listing__pagination a.pagination-button');
452				if (!link) return;
453
454				e.preventDefault();
455
456				var dataPage = link.getAttribute('data-page');
457				if (dataPage) {
458					state.paged = parseInt(dataPage, 10) || 1;
459				} else {
460					var href = link.getAttribute('href');
461					if (!href) return;
462					var url = new URL(href, window.location.origin);
463					var pageParam = url.searchParams.get('paged');
464					if (pageParam) {
465						state.paged = parseInt(pageParam, 10) || 1;
466					} else {
467						var match = href.match(/\/page\/(\d+)/);
468						state.paged = match ? parseInt(match[1], 10) : 1;
469					}
470				}
471
472				pushUrl();
473				fetchResults();
474
475				block.scrollIntoView({ behavior: 'smooth', block: 'start' });
476			});
477		}
478
479		function fetchResults() {
480			if (!resultsEl) return;
481
482			resultsEl.classList.add('is-loading');
483
484			var formData = new FormData();
485			formData.append('action', 'archive_listing_filter');
486			formData.append('nonce', config.nonce);
487			formData.append('posts_per_page', config.postsPerPage);
488			formData.append('orderby', config.orderby);
489			formData.append('order', config.order);
490			formData.append('paged', state.paged);
491			formData.append('search', state.search);
492			formData.append('show_aside', config.showAside ? '1' : '0');
493			// Lead flags (re-asserted server-side): view_lead marks the
494			// unfiltered VIEW whose page 1 carries the full-width lead card —
495			// the server derives the page offset windows from it on every
496			// page. with_lead marks page 1 itself (the lead render request).
497			formData.append('view_lead', isLeadView() ? '1' : '0');
498			formData.append('with_lead', isLeadState() ? '1' : '0');
499			formData.append('page_slug', basePath.split('/').filter(Boolean).pop() || '');
500
501			var activeTypes = state.postTypes.length > 0 ? state.postTypes : config.postTypes;
502			activeTypes.forEach(function (pt) {
503				formData.append('post_types[]', pt);
504			});
505
506			Object.keys(state.taxonomies).forEach(function (tax) {
507				state.taxonomies[tax].forEach(function (term) {
508					formData.append('taxonomies[' + tax + '][]', term);
509				});
510			});
511
512			// Forward the editor-defined base scope so the AJAX query stays scoped.
513			function appendScopeMap(key, map) {
514				if (!map || typeof map !== 'object') return;
515				Object.keys(map).forEach(function (tax) {
516					var terms = map[tax];
517					if (!Array.isArray(terms)) return;
518					terms.forEach(function (slug) {
519						formData.append(key + '[' + tax + '][]', slug);
520					});
521				});
522			}
523			appendScopeMap('restrict_map', config.restrictMap);
524			appendScopeMap('exclude_map', config.excludeMap);
525			if (Array.isArray(config.excludePostTypes)) {
526				config.excludePostTypes.forEach(function (pt) {
527					formData.append('exclude_post_types[]', pt);
528				});
529			}
530
531			fetch(config.ajaxUrl, {
532				method: 'POST',
533				body: formData,
534				credentials: 'same-origin'
535			})
536			.then(function (response) {
537				return response.json();
538			})
539			.then(function (data) {
540				if (data.success) {
541					resultsEl.innerHTML = data.data.html;
542					// Full-width lead slot: filled on page 1 of the unfiltered
543					// view, cleared (and hidden) in every other state.
544					var leadEl = block.querySelector('[data-alb-lead]');
545					if (leadEl) {
546						var leadHtml = data.data.lead || '';
547						leadEl.innerHTML = leadHtml;
548						leadEl.hidden = leadHtml === '';
549					}
550					if (paginationEl) {
551						paginationEl.innerHTML = data.data.pagination;
552					}
553					updateAside(data.data.aside || '');
554					applyCardTitleTooltips(block);
555				}
556				resultsEl.classList.remove('is-loading');
557			})
558			.catch(function () {
559				resultsEl.classList.remove('is-loading');
560			});
561		}
562
563		function isLeadView() {
564			return !state.search &&
565				state.postTypes.length === 0 &&
566				Object.keys(state.taxonomies).length === 0;
567		}
568
569		function isLeadState() {
570			return state.paged === 1 && isLeadView();
571		}
572
573		function updateAside(html) {
574			var asideEl = block.querySelector('.archive-listing__aside-panel');
575			if (!config.showAside) return;
576
577			if (html) {
578				if (asideEl) {
579					asideEl.outerHTML = html;
580				} else if (listEl) {
581					listEl.insertAdjacentHTML('afterend', html);
582				}
583			} else if (asideEl) {
584				asideEl.remove();
585			}
586
587			initAsideScroll();
588		}
589
590		function initAsideScroll() {
591			// The aside is now a static, user-scrollable list (no marquee).
592			// Kept as a hook so ajax refreshes don't error.
593		}
594
595		initAsideScroll();
596	}
597
598	// Standalone marquee init for [data-aside-scroll] elements outside of a
599	// configured archive-listing-block (e.g., the related-posts aside on single.php).
600	document.addEventListener('DOMContentLoaded', function () {
601		var tracks = document.querySelectorAll('[data-aside-scroll]');
602		if (!tracks.length) return;
603
604		tracks.forEach(function (track) {
605			var owningBlock = track.closest('.archive-listing-block[data-alb-config]');
606			if (owningBlock) return;
607
608			initStandaloneAsideScroll(track);
609		});
610	});
611
612	function initStandaloneAsideScroll(track) {
613		// Static aside: nothing to initialize (marquee removed per MI feedback).
614	}
615})();

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.