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.