config filtering

Signed-off-by: Andy Miller <rhuk@mac.com>
This commit is contained in:
Andy Miller
2026-03-17 14:52:13 -06:00
parent 81ac5b4e65
commit 969f00a016
4 changed files with 226 additions and 0 deletions

View File

@@ -56,6 +56,7 @@ PLUGIN_ADMIN:
MODAL_UPDATE_GRAV_CONFIRMATION_REQUIRED_DESC: "You are about to upgrade Grav to the latest version available. Would you like to continue?"
ADD_FILTERS: "Add Filters"
SEARCH_PAGES: "Search Pages"
SEARCH_CONFIGURATION: "Search settings..."
VERSION: "Version"
WAS_MADE_WITH: "Was made with"
BY: "By"

View File

@@ -0,0 +1,96 @@
/* Config Search Feature */
.config-search-wrapper {
padding: 0.6rem 1.5rem;
border-bottom: 1px solid #e0e0e0;
background: #fafafa;
}
.config-search-inner {
position: relative;
display: inline-flex;
align-items: center;
max-width: 360px;
width: 100%;
}
.config-search-icon {
position: absolute;
left: 10px;
color: #999;
pointer-events: none;
font-size: 0.8rem;
}
#config-search-input {
width: 100%;
padding: 6px 28px 6px 30px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 0.85rem;
background: #fff;
color: #333;
outline: none;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
line-height: 1.4;
}
#config-search-input:focus {
border-color: #4990e2;
box-shadow: 0 0 0 2px rgba(73, 144, 226, 0.15);
}
#config-search-input::placeholder {
color: #bbb;
}
.config-search-clear {
position: absolute;
right: 7px;
background: none;
border: none;
cursor: pointer;
color: #bbb;
display: none;
padding: 2px 3px;
font-size: 0.75rem;
line-height: 1;
align-items: center;
justify-content: center;
}
.config-search-clear:hover {
color: #666;
}
/* Badge showing match count in tab nav link */
.search-count-badge {
display: inline-block;
background: #4990e2;
color: #fff;
font-size: 0.6rem;
font-weight: bold;
padding: 1px 5px;
border-radius: 10px;
margin-left: 6px;
vertical-align: middle;
line-height: 1.6;
min-width: 16px;
text-align: center;
}
/* Hide tabs with no matches */
.tabs-nav .tab__link.search-no-match {
display: none !important;
}
/* Highlight matching form field rows */
.form-field.grid.search-highlight {
background-color: rgba(255, 218, 30, 0.18);
border-radius: 3px;
box-shadow: inset 3px 0 0 rgba(240, 190, 0, 0.5);
}
/* Hide sections with no matches on flat (non-tabbed) layouts */
.block.search-hidden {
display: none;
}

113
themes/grav/js/config-search.js vendored Normal file
View File

@@ -0,0 +1,113 @@
(function () {
var input = document.getElementById('config-search-input');
var clearBtn = document.getElementById('config-search-clear');
if (!input || !clearBtn) return;
var form = document.getElementById('blueprints');
if (!form) return;
var debounceTimer;
function normalize(text) {
return text.toLowerCase().replace(/\s+/g, ' ').trim();
}
function clearState() {
form.querySelectorAll('.form-field.grid.search-highlight').forEach(function (el) {
el.classList.remove('search-highlight');
});
form.querySelectorAll('.search-count-badge').forEach(function (el) {
el.remove();
});
form.querySelectorAll('.tab__link.search-no-match').forEach(function (el) {
el.classList.remove('search-no-match');
});
form.querySelectorAll('.block.search-hidden').forEach(function (el) {
el.classList.remove('search-hidden');
});
}
function matchFields(container, query) {
var count = 0;
container.querySelectorAll('.form-field.grid').forEach(function (field) {
var label = field.querySelector('.form-label');
if (label && normalize(label.textContent).indexOf(query) !== -1) {
field.classList.add('search-highlight');
count++;
}
});
return count;
}
function searchTabbed(container, query) {
var tabLinks = container.querySelectorAll('.tabs-nav .tab__link');
var firstMatch = null;
tabLinks.forEach(function (link) {
var tabId = link.getAttribute('data-tabid');
var content = tabId ? document.getElementById(tabId) : null;
if (!content) return;
var count = matchFields(content, query);
if (count > 0) {
link.classList.remove('search-no-match');
var span = link.querySelector('span');
if (span) {
var badge = document.createElement('span');
badge.className = 'search-count-badge';
badge.textContent = count;
span.appendChild(badge);
}
if (!firstMatch) firstMatch = link;
} else {
link.classList.add('search-no-match');
}
});
// Auto-switch if active tab has no matches
if (firstMatch) {
var active = container.querySelector('.tabs-nav .tab__link.active');
if (!active || active.classList.contains('search-no-match')) {
firstMatch.dispatchEvent(new MouseEvent('click', { bubbles: true }));
}
}
}
function searchFlat(query) {
form.querySelectorAll('.block.block-section').forEach(function (block) {
var count = matchFields(block, query);
if (count === 0) {
block.classList.add('search-hidden');
}
});
}
function runSearch(rawQuery) {
clearState();
var query = normalize(rawQuery);
if (!query) return;
var sideTabs = form.querySelector('.form-tabs.side-tabs');
if (sideTabs) {
searchTabbed(sideTabs, query);
} else {
searchFlat(query);
}
}
input.addEventListener('input', function () {
var val = this.value;
clearBtn.style.display = val ? 'inline-flex' : 'none';
clearTimeout(debounceTimer);
debounceTimer = setTimeout(function () { runSearch(val); }, 200);
});
clearBtn.addEventListener('click', function () {
input.value = '';
clearBtn.style.display = 'none';
runSearch('');
input.focus();
});
}());

View File

@@ -7,6 +7,7 @@
{% do admin.redirect('config/' ~ config_slug, 302) %}
{% endif %}
{% set isInfo = (config_slug == 'info') %}
{% set isSearchable = config_slug in ['system', 'site'] %}
{% set tab_title_string = "PLUGIN_ADMIN." ~ config_slug|upper %}
{% set tab_title = (tab_title_string|t == tab_title_string ? config_slug|capitalize : tab_title_string|t) ?: 'Not Found' %}
@@ -18,10 +19,16 @@
{% block stylesheets %}
{% do assets.addCss(theme_url ~ '/css/codemirror/codemirror.css') %}
{% if isSearchable %}
{% do assets.addCss(theme_url ~ '/css-compiled/config-search.css') %}
{% endif %}
{{ parent() }}
{% endblock %}
{% block javascripts %}
{% if isSearchable %}
{% do assets.addJs(theme_url ~ '/js/config-search.js', {'group': 'bottom', 'loading': 'defer'}) %}
{% endif %}
{{ parent() }}
{% endblock %}
@@ -50,6 +57,15 @@
{% endfor %}
</div>
</div>
{% if isSearchable %}
<div class="config-search-wrapper">
<div class="config-search-inner">
<i class="config-search-icon fa fa-search"></i>
<input type="search" id="config-search-input" placeholder="{{ 'PLUGIN_ADMIN.SEARCH_CONFIGURATION'|t }}" autocomplete="off">
<button type="button" class="config-search-clear" id="config-search-clear" aria-label="Clear search"><i class="fa fa-times"></i></button>
</div>
</div>
{% endif %}
{% endblock %}
{% block content %}