PageSourceSearch

https://zededa.com/wp-content/themes/Zededa/assets/js/resource-browser-widget.js?ver=1790835109

js zededa.com collected 2026-10-01 10:44:46 UTC 12,744 bytes, 261 lines download raw bytes

1/** Resource Browser: one state and one AJAX request for every filter. */
2(function ($) {
3	'use strict';
4
5	function buildRequestParams(state) {
6		return {
7			action: 'zededa_resource_browse',
8			search: String(state.search || '').trim(),
9			post_types: (state.types || []).slice(),
10			tags: (state.tags || []).slice(),
11			order: state.order === 'ASC' ? 'ASC' : 'DESC',
12			page: Math.max(1, Number(state.page) || 1),
13			posts_per_page: state.postsPerPage || 8,
14			visible_tabs: (state.visibleTypes || []).join(','),
15			tag: state.archiveTag || '',
16			taxonomy: state.archiveTaxonomy || 'category',
17		};
18	}
19
20	function formatCount(displayed, total) {
21		return 'Showing ' + Number(displayed).toLocaleString('en-US') + ' of ' + Number(total).toLocaleString('en-US') + ' resources';
22	}
23
24	function ResourceBrowser(root) {
25		this.root = root;
26		if (!root) return;
27		if (root.dataset.resourceBrowserInit === '1') return;
28		root.dataset.resourceBrowserInit = '1';
29		var config = JSON.parse(root.getAttribute('data-widget-config') || '{}');
30		this.config = config;
31		this.grid = root.querySelector('.zededa-resource-browser__grid');
32		this.filters = root.querySelector('.zededa-resource-browser__filters');
33		this.categories = root.querySelector('.zededa-resource-browser__categories');
34		this.pagination = root.querySelector('.zededa-resource-browser__pagination');
35		this.trigger = root.querySelector('.zededa-resource-browser__media-trigger');
36		this.panel = root.querySelector('.zededa-resource-browser__media-panel');
37		this.count = root.querySelector('.zededa-resource-browser__count');
38		this.loading = root.querySelector('.zededa-resource-browser__loading');
39		this.maxPages = Number(this.pagination && this.pagination.dataset.maxPages) || 1;
40		var visibleTypes = Array.isArray(config.visibleTabs) ? config.visibleTabs : [];
41		var preFilter = visibleTypes.indexOf(config.preFilter) !== -1 ? config.preFilter : '';
42		var urlSearch = new URLSearchParams(window.location.search).get('rbs') || '';
43		this.state = {
44			search: urlSearch,
45			types: preFilter ? [preFilter] : [],
46			tags: [],
47			order: 'DESC',
48			page: Number(config.currentPage) || 1,
49			postsPerPage: config.postsPerPage || 8,
50			visibleTypes: visibleTypes,
51			archiveTag: config.archiveTag || '',
52			archiveTaxonomy: config.archiveTaxonomy || 'category',
53		};
54		this.bindEvents();
55		this.syncControls();
56		if (urlSearch) this.fetchResults(true);
57	}
58
59	ResourceBrowser.prototype.syncControls = function () {
60		var self = this;
61		this.root.querySelectorAll('.zededa-resource-browser__media-panel input[type="checkbox"]').forEach(function (input) {
62			input.checked = self.state.types.indexOf(input.value) !== -1;
63		});
64		this.root.querySelectorAll('.zededa-resource-browser__chip').forEach(function (chip) {
65			var id = Number(chip.dataset.tag);
66			chip.setAttribute('aria-pressed', String(id ? self.state.tags.indexOf(id) !== -1 : self.state.tags.length === 0));
67		});
68		var input = this.root.querySelector('.zededa-resource-browser__search-input');
69		if (input) input.value = this.state.search;
70		this.syncChipOverflow();
71	};
72
73	// Chips past the top 10 stay hidden until "Show all categories" — except a selected one,
74	// so the active filter is always on screen.
75	ResourceBrowser.prototype.syncChipOverflow = function () {
76		var toggle = this.root.querySelector('.zededa-resource-browser__chips-toggle');
77		if (!toggle) return;
78		var expanded = toggle.getAttribute('aria-expanded') === 'true';
79		toggle.textContent = expanded ? toggle.dataset.labelLess : toggle.dataset.labelMore;
80		this.root.querySelectorAll('.zededa-resource-browser__chip--overflow').forEach(function (chip) {
81			chip.hidden = !expanded && chip.getAttribute('aria-pressed') !== 'true';
82		});
83	};
84
85	ResourceBrowser.prototype.selectTag = function (id, name) {
86		if (!this.categories.querySelector('.zededa-resource-browser__chip[data-tag="' + id + '"]')) {
87			var chip = document.createElement('button');
88			chip.type = 'button';
89			chip.className = 'zededa-resource-browser__chip';
90			chip.dataset.tag = String(id);
91			chip.textContent = name;
92			this.categories.insertBefore(chip, this.categories.querySelector('.zededa-resource-browser__chips-toggle'));
93		}
94		this.state.tags = [id];
95		this.syncControls();
96		this.filterChanged();
97		if (this.filters && this.filters.scrollIntoView) this.filters.scrollIntoView({ behavior: 'smooth', block: 'start' });
98	};
99
100	ResourceBrowser.prototype.setDropdown = function (open, refocus) {
101		if (!this.trigger || !this.panel) return;
102		this.trigger.setAttribute('aria-expanded', String(open));
103		this.panel.hidden = !open;
104		if (!open && refocus) this.trigger.focus();
105	};
106
107	ResourceBrowser.prototype.filterChanged = function () {
108		clearTimeout(this.searchTimer);
109		var input = this.root.querySelector('.zededa-resource-browser__search-input');
110		if (input) this.state.search = input.value.trim();
111		this.state.page = 1;
112		this.fetchResults();
113	};
114
115	ResourceBrowser.prototype.bindEvents = function () {
116		var self = this;
117		var input = this.root.querySelector('.zededa-resource-browser__search-input');
118		if (input) input.addEventListener('input', function () {
119			clearTimeout(self.searchTimer);
120			self.searchTimer = setTimeout(function () {
121				self.state.search = input.value.trim();
122				self.filterChanged();
123			}, 350);
124		});
125		var sort = this.root.querySelector('.zededa-resource-browser__sort');
126		if (sort) sort.addEventListener('change', function () {
127			self.state.order = sort.value === 'ASC' ? 'ASC' : 'DESC';
128			self.filterChanged();
129		});
130		// Delegated so chips added for a clicked card tag work too.
131		if (this.categories) this.categories.addEventListener('click', function (event) {
132			var chip = event.target.closest('.zededa-resource-browser__chip');
133			if (!chip) return;
134			var id = Number(chip.dataset.tag);
135			if (!id) self.state.tags = [];
136			else if (self.state.tags.indexOf(id) !== -1) self.state.tags = self.state.tags.filter(function (tag) { return tag !== id; });
137			else self.state.tags = self.state.tags.concat(id);
138			self.syncControls();
139			self.filterChanged();
140		});
141		// A card's tag filters the browser instead of opening the tag archive. Without a
142		// category nav (tag archive pages) the link navigates as usual.
143		if (this.categories && this.grid) this.grid.addEventListener('click', function (event) {
144			var link = event.target.closest('.zededa-card__tag[data-tag]');
145			var id = link && Number(link.dataset.tag);
146			if (!id) return;
147			event.preventDefault();
148			self.selectTag(id, link.textContent.trim());
149		});
150		var chipsToggle = this.root.querySelector('.zededa-resource-browser__chips-toggle');
151		if (chipsToggle) chipsToggle.addEventListener('click', function () {
152			chipsToggle.setAttribute('aria-expanded', String(chipsToggle.getAttribute('aria-expanded') !== 'true'));
153			self.syncChipOverflow();
154		});
155		if (this.trigger && this.panel) {
156			this.trigger.addEventListener('click', function () { self.setDropdown(self.panel.hidden); });
157			var options = Array.from(this.panel.querySelectorAll('input[type="checkbox"]'));
158			this.trigger.addEventListener('keydown', function (event) {
159				if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
160					event.preventDefault(); self.setDropdown(true);
161					if (options.length) options[event.key === 'ArrowDown' ? 0 : options.length - 1].focus();
162				} else if (event.key === 'Escape') self.setDropdown(false, true);
163			});
164			this.panel.addEventListener('keydown', function (event) {
165				if (event.key === 'Escape') { event.preventDefault(); self.setDropdown(false, true); }
166				else if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
167					event.preventDefault();
168					var index = options.indexOf(event.target);
169					if (index !== -1) options[(index + (event.key === 'ArrowDown' ? 1 : -1) + options.length) % options.length].focus();
170				} else if (event.key === ' ' && event.target.matches('input[type="checkbox"]')) {
171					event.preventDefault(); event.target.checked = !event.target.checked;
172					event.target.dispatchEvent(new Event('change', { bubbles: true }));
173				}
174			});
175			this.panel.addEventListener('change', function () {
176				self.state.types = options.filter(function (option) { return option.checked; }).map(function (option) { return option.value; });
177				self.filterChanged();
178			});
179			this.outsideHandler = function (event) {
180				if (!self.root.contains(event.target) || !self.root.querySelector('.zededa-resource-browser__media').contains(event.target)) self.setDropdown(false);
181			};
182			document.addEventListener('click', this.outsideHandler);
183			// Esc must work even when a mouse click left focus outside the dropdown (Safari never focuses clicked buttons).
184			// Capture phase + stopPropagation: the navigation widget's document-level Esc handler blurs the active element.
185			this.escapeHandler = function (event) {
186				if (event.key !== 'Escape' || self.panel.hidden) return;
187				event.preventDefault();
188				event.stopPropagation();
189				self.setDropdown(false, true);
190			};
191			document.addEventListener('keydown', this.escapeHandler, true);
192		}
193		if (this.pagination) this.pagination.addEventListener('click', function (event) {
194			var link = event.target.closest('.zededa-resource-browser__page-btn');
195			if (!link) return;
196			event.preventDefault();
197			if (link.getAttribute('aria-disabled') === 'true') return;
198			self.state.page += link.classList.contains('zededa-resource-browser__page-btn--next') ? 1 : -1;
199			self.fetchResults();
200		});
201	};
202
203	ResourceBrowser.prototype.pageUrl = function (page) {
204		var base = (this.config.baseUrl || window.location.pathname).replace(/\/$/, '');
205		var url = base + (page > 1 ? '/page/' + page : '') + '/';
206		return url + (this.state.search ? '?rbs=' + encodeURIComponent(this.state.search) : '');
207	};
208
209	ResourceBrowser.prototype.updatePagination = function () {
210		if (!this.pagination) return;
211		this.pagination.hidden = this.maxPages <= 1;
212		var current = this.pagination.querySelector('.zededa-resource-browser__page-current');
213		var total = this.pagination.querySelector('.zededa-resource-browser__page-total');
214		if (current) current.textContent = this.state.page;
215		if (total) total.textContent = this.maxPages;
216		var self = this;
217		[['prev', this.state.page - 1], ['next', this.state.page + 1]].forEac
217h(function (entry) {
218			var link = self.pagination.querySelector('.zededa-resource-browser__page-btn--' + entry[0]);
219			if (!link) return;
220			var disabled = entry[1] < 1 || entry[1] > self.maxPages;
221			link.setAttribute('aria-disabled', String(disabled));
222			if (disabled) link.removeAttribute('href'); else link.href = self.pageUrl(entry[1]);
223		});
224	};
225
226	ResourceBrowser.prototype.fetchResults = function (skipScroll) {
227		var self = this;
228		if (this.request && this.request.abort) this.request.abort();
229		this.root.classList.add('zededa-resource-browser--loading');
230		if (this.loading) { this.loading.hidden = false; this.loading.setAttribute('aria-hidden', 'false'); }
231		this.request = $.ajax({
232			url: this.config.ajaxUrl, type: 'POST', data: buildRequestParams(this.state),
233			success: function (response) {
234				var result = response.data || {};
235				var found = Number(result.found_posts) || 0;
236				self.grid.innerHTML = response.success ? result.html : '<p class="zededa-resource-browser__empty">No resources found.</p>';
237				self.maxPages = Number(result.max_pages) || 1;
238				self.updatePagination();
239				if (self.count) self.count.textContent = formatCount(self.grid.querySelectorAll('.zededa-resource-browser__card').length, found);
240				window.history.replaceState(null, '', self.pageUrl(self.state.page));
241			},
242			error: function (_xhr, status) {
243				if (status !== 'abort') self.grid.innerHTML = '<p class="zededa-resource-browser__empty">Error loading resources. Please try again.</p>';
244			},
245			complete: function () {
246				self.root.classList.remove('zededa-resource-browser--loading');
247				if (self.loading) { self.loading.hidden = true; self.loading.setAttribute('aria-hidden', 'true'); }
248				if (!skipScroll && self.grid && self.grid.scrollIntoView) self.grid.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
249			},
250		});
251	};
252
253	$(window).on('elementor/frontend/init', function () {
254		if (typeof elementorFrontend !== 'undefined') elementorFrontend.hooks.addAction('frontend/element_ready/zededa_resource_browser.default', function ($scope) {
255			$scope.find('.zededa-resource-browser').each(function () { new ResourceBrowser(this); });
256		});
257	});
258	$(function () { document.querySelectorAll('.zededa-resource-browser').forEach(function (root) { new ResourceBrowser(root); }); });
259
260	if (typeof module !== 'undefined' && module.exports) module.exports = { buildRequestParams: buildRequestParams, formatCount: formatCount, ResourceBrowser: ResourceBrowser };
261})(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.