baseline
This commit is contained in:
168
web/js/components/app-global-search.js
Normal file
168
web/js/components/app-global-search.js
Normal file
@@ -0,0 +1,168 @@
|
||||
const input = document.querySelector('#side-search');
|
||||
const resultsEl = document.querySelector('[data-global-search-results]');
|
||||
|
||||
if (!input || !resultsEl) {
|
||||
if (!input) console.warn('[GlobalSearch] #side-search input not found');
|
||||
if (!resultsEl) console.warn('[GlobalSearch] [data-global-search-results] container not found');
|
||||
} else {
|
||||
const panel = document.querySelector('#aside-panel-search');
|
||||
const toggleButton = document.querySelector('[data-search-details-toggle]');
|
||||
const toggleIcon = document.querySelector('[data-search-details-icon]');
|
||||
const appBase = window.APP_BASE || document.baseURI;
|
||||
const endpoint = new URL('admin/search/data', appBase);
|
||||
const resultsPage = new URL('search', appBase);
|
||||
const minLength = 2;
|
||||
let pending = null;
|
||||
|
||||
const itemsByKey = new Map();
|
||||
resultsEl.querySelectorAll('[data-search-key]').forEach((item) => {
|
||||
const key = item.getAttribute('data-search-key');
|
||||
if (key) itemsByKey.set(key, item);
|
||||
});
|
||||
if (itemsByKey.size === 0) {
|
||||
console.warn('[GlobalSearch] No [data-search-key] elements found in results container');
|
||||
}
|
||||
|
||||
const setLoadingState = (value) => {
|
||||
resultsEl.querySelectorAll('[data-search-count]').forEach((el) => {
|
||||
el.textContent = value;
|
||||
});
|
||||
};
|
||||
|
||||
const render = (items, query) => {
|
||||
items.forEach((item) => {
|
||||
const li = itemsByKey.get(item.key);
|
||||
if (!li) {
|
||||
console.warn('[GlobalSearch] No element found for key:', item.key);
|
||||
return;
|
||||
}
|
||||
const countEl = li.querySelector('[data-search-count]');
|
||||
const link = li.querySelector('a');
|
||||
const previewEl = li.querySelector('[data-search-preview]');
|
||||
const base = item.key === 'pages' && item.url ? item.url : (li.getAttribute('data-search-base') || item.url);
|
||||
const url = new URL(base, appBase);
|
||||
if (item.key !== 'pages') {
|
||||
url.searchParams.set('search', query);
|
||||
}
|
||||
if (countEl) countEl.textContent = item.count.toString();
|
||||
if (link) {
|
||||
link.href = item.count > 0 ? url.toString() : base;
|
||||
link.setAttribute('aria-disabled', item.count > 0 ? 'false' : 'true');
|
||||
}
|
||||
if (previewEl) {
|
||||
previewEl.innerHTML = '';
|
||||
if (Array.isArray(item.items) && item.items.length > 0) {
|
||||
item.items.forEach((previewItem, idx) => {
|
||||
if (!previewItem || !previewItem.url) {
|
||||
console.warn('[GlobalSearch] Preview item missing url at index', idx, 'for key:', item.key, previewItem);
|
||||
return;
|
||||
}
|
||||
const previewLi = document.createElement('li');
|
||||
const previewLink = document.createElement('a');
|
||||
previewLink.href = previewItem.url;
|
||||
previewLink.textContent = previewItem.label || '';
|
||||
previewLi.appendChild(previewLink);
|
||||
previewEl.appendChild(previewLi);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (item.count > 0) {
|
||||
li.hidden = false;
|
||||
} else {
|
||||
li.hidden = true;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const clear = () => {
|
||||
setLoadingState('0');
|
||||
resultsEl.querySelectorAll('a').forEach((link) => {
|
||||
link.setAttribute('aria-disabled', 'true');
|
||||
const base = link.closest('[data-search-base]')?.getAttribute('data-search-base');
|
||||
link.setAttribute('href', base || '#');
|
||||
});
|
||||
resultsEl.querySelectorAll('[data-search-preview]').forEach((list) => {
|
||||
list.innerHTML = '';
|
||||
});
|
||||
resultsEl.querySelectorAll('[data-search-key]').forEach((li) => {
|
||||
li.hidden = false;
|
||||
});
|
||||
};
|
||||
|
||||
const fetchCounts = async (value) => {
|
||||
const url = new URL(endpoint.toString());
|
||||
url.searchParams.set('q', value);
|
||||
setLoadingState('…');
|
||||
const response = await fetch(url.toString(), { headers: { 'Accept': 'application/json' } });
|
||||
if (!response.ok) {
|
||||
console.warn('[GlobalSearch] Fetch failed with status:', response.status, response.statusText);
|
||||
clear();
|
||||
return;
|
||||
}
|
||||
const data = await response.json();
|
||||
if (Array.isArray(data)) {
|
||||
render(data, value);
|
||||
} else {
|
||||
console.warn('[GlobalSearch] Invalid response format, expected array but got:', typeof data, data);
|
||||
clear();
|
||||
}
|
||||
};
|
||||
|
||||
const onInput = () => {
|
||||
const value = input.value.trim();
|
||||
if (value.length < minLength) {
|
||||
clear();
|
||||
return;
|
||||
}
|
||||
if (pending) window.clearTimeout(pending);
|
||||
pending = window.setTimeout(() => {
|
||||
fetchCounts(value).catch((err) => {
|
||||
console.warn('[GlobalSearch] Fetch error:', err);
|
||||
clear();
|
||||
});
|
||||
}, 300);
|
||||
};
|
||||
|
||||
input.addEventListener('input', onInput);
|
||||
const submitSearch = () => {
|
||||
const value = input.value.trim();
|
||||
if (value.length < minLength) return;
|
||||
resultsPage.searchParams.set('search', value);
|
||||
window.location.href = resultsPage.toString();
|
||||
};
|
||||
|
||||
input.addEventListener('keydown', (event) => {
|
||||
if (event.key !== 'Enter') return;
|
||||
event.preventDefault();
|
||||
submitSearch();
|
||||
});
|
||||
|
||||
const form = input.closest('form');
|
||||
if (form) {
|
||||
form.addEventListener('submit', (event) => {
|
||||
event.preventDefault();
|
||||
submitSearch();
|
||||
});
|
||||
}
|
||||
|
||||
if (toggleButton && panel) {
|
||||
toggleButton.addEventListener('click', () => {
|
||||
panel.classList.toggle('search-details-hidden');
|
||||
if (toggleIcon) {
|
||||
toggleIcon.classList.toggle('bi-dash-square');
|
||||
toggleIcon.classList.toggle('bi-plus-square');
|
||||
}
|
||||
});
|
||||
} else if (toggleButton || panel) {
|
||||
if (!toggleButton) console.warn('[GlobalSearch] [data-search-details-toggle] button not found');
|
||||
if (!panel) console.warn('[GlobalSearch] #aside-panel-search panel not found');
|
||||
}
|
||||
|
||||
// Seed from current URL search param if present
|
||||
const currentUrl = new URL(window.location.href);
|
||||
const preset = currentUrl.searchParams.get('search');
|
||||
if (preset && !input.value) {
|
||||
input.value = preset;
|
||||
onInput();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user