diff --git a/assets/scss/custom.scss b/assets/scss/custom.scss index 29d826e4551..ec641f506d2 100644 --- a/assets/scss/custom.scss +++ b/assets/scss/custom.scss @@ -2045,6 +2045,34 @@ h1.clusters-site-heading.clusters-hub-page__title { .fr-specificity-toggle input { margin: 0; } +.fr-clear-btn { + margin-left: auto; + align-self: center; + font-family: inherit; + font-size: 0.62rem; + font-weight: 600; + padding: 0.18rem 0.5rem; + border-radius: 3px; + border: 1px solid rgba(0,64,85,0.25); + background: transparent; + color: #004055; + cursor: pointer; + white-space: nowrap; + transition: background 0.15s, color 0.15s, border-color 0.15s; +} +.fr-clear-btn::before { + content: "\00d7"; + margin-right: 0.3rem; + font-weight: 700; +} +.fr-clear-btn:hover { + background: #004055; + color: #fff; + border-color: #004055; +} +.fr-clear-btn[hidden] { + display: none; +} /* ---- Search input ---- */ .fr-search-wrap { diff --git a/layouts/partials/clusters/clusters_controls.html b/layouts/partials/clusters/clusters_controls.html index a8bd2db81d4..673037fa735 100644 --- a/layouts/partials/clusters/clusters_controls.html +++ b/layouts/partials/clusters/clusters_controls.html @@ -32,6 +32,8 @@ Show specialist resources + {{/* Inline search results panel (kept for cluster/subcluster name search) */}} diff --git a/static/js/featured-resources.js b/static/js/featured-resources.js index 7aff1938817..6c92d14fc16 100644 --- a/static/js/featured-resources.js +++ b/static/js/featured-resources.js @@ -705,33 +705,45 @@ var typeGroup = document.getElementById('fr-global-type-tags'); var specCheckbox = document.getElementById('fr-global-specificity-checkbox'); var searchInput = document.getElementById('clusters-inline-search-input'); + var clearBtn = document.getElementById('fr-global-clear'); // --- Pre-cache card data so filtering never queries the DOM for text --- var sectionCache = []; document.querySelectorAll('.acc-section').forEach(function (section) { + var header = section.querySelector('.acc-header'); + var body = section.querySelector('.acc-body'); + + /* Own text (heading + description) - a query matching only this, not any + card, still means the section is relevant and shouldn't disable/collapse. */ + var labelEl = header ? header.querySelector('.acc-label') : null; + var descEl = section.querySelector('.sc-description'); + var ownText = ((labelEl ? labelEl.textContent : '') + ' ' + (descEl ? descEl.textContent : '')).toLowerCase(); + var container = section.querySelector('.fr-cards-list'); - if (!container) return; - var cardEls = container.querySelectorAll('.fc-card, .fr-card'); var cards = []; - cardEls.forEach(function (card) { - var title = (card.querySelector('.fc-title') || card.querySelector('.fr-title') || {}).textContent || ''; - var summary = (card.querySelector('.fc-summary') || card.querySelector('.fr-summary') || {}).textContent || ''; - cards.push({ - el: card, - focus: card.getAttribute('data-focus'), - type: card.getAttribute('data-type'), - spec: card.getAttribute('data-specificity'), - text: (title + ' ' + summary).toLowerCase() + if (container) { + container.querySelectorAll('.fc-card, .fr-card').forEach(function (card) { + var title = (card.querySelector('.fc-title') || card.querySelector('.fr-title') || {}).textContent || ''; + var summary = (card.querySelector('.fc-summary') || card.querySelector('.fr-summary') || {}).textContent || ''; + cards.push({ + el: card, + focus: card.getAttribute('data-focus'), + type: card.getAttribute('data-type'), + spec: card.getAttribute('data-specificity'), + text: (title + ' ' + summary).toLowerCase() + }); }); - }); - var header = section.querySelector('.acc-header'); - var body = section.querySelector('.acc-body'); + } + + if (!container && !ownText.trim()) return; + sectionCache.push({ el: section, countEl: section.querySelector('.acc-count-match'), header: header, body: body, chevron: header ? header.querySelector('.acc-chevron') : null, + ownText: ownText, cards: cards }); }); @@ -750,6 +762,24 @@ if (specCheckbox) specCheckbox.addEventListener('change', applyGlobalFilters); + if (clearBtn) { + clearBtn.addEventListener('click', function () { + if (searchInput) searchInput.value = ''; + resetGroupToAll(focusGroup); + resetGroupToAll(typeGroup); + if (specCheckbox) specCheckbox.checked = false; + applyGlobalFilters(); + if (searchInput) searchInput.focus(); + }); + } + + function resetGroupToAll(group) { + if (!group) return; + group.querySelectorAll('.fr-tag-btn').forEach(function (b) { + b.classList.toggle('active', b.getAttribute('data-value') === 'all'); + }); + } + if (searchInput) { var debounce; searchInput.addEventListener('input', function () { @@ -773,6 +803,7 @@ var query = searchInput ? searchInput.value.toLowerCase().trim() : ''; var tokens = query ? query.split(/\s+/) : []; var isFiltered = activeFocus !== 'all' || activeType !== 'all' || tokens.length > 0; + if (clearBtn) clearBtn.hidden = !isFiltered; // --- Pass 1: compute visibility from cached data (no DOM reads) --- var sectionResults = []; @@ -793,7 +824,19 @@ cardVis[c] = show; if (show) matchCount++; } - sectionResults.push({ matchCount: matchCount, cardVis: cardVis }); + + /* A query matching only the section's own heading/description (no card) + still means it's relevant - keep it open rather than disabling it. + Only applies with no active facet, since facets are card-specific. */ + var ownTextMatches = false; + if (tokens.length > 0 && activeFocus === 'all' && activeType === 'all' && sec.ownText) { + ownTextMatches = true; + for (var t2 = 0; t2 < tokens.length; t2++) { + if (sec.ownText.indexOf(tokens[t2]) === -1) { ownTextMatches = false; break; } + } + } + + sectionResults.push({ matchCount: matchCount, cardVis: cardVis, ownTextMatches: ownTextMatches }); } // --- Pass 2: batch all DOM writes --- @@ -812,7 +855,7 @@ var chevron = sec.chevron; if (!header || !body) continue; - if (res.matchCount === 0) { + if (res.matchCount === 0 && !res.ownTextMatches) { if (!header.classList.contains('acc-disabled')) { header.dataset.wasOpen = body.classList.contains('acc-collapsed') ? '0' : '1'; }