search added
This commit is contained in:
@@ -265,7 +265,10 @@
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="sidebar-title text-xl font-bold text-white">{{.app_name}}</h1>
|
||||
<div class="flex items-center space-x-2 items-center">
|
||||
<div class="sidebar-actions flex items-center space-x-2">
|
||||
<div class="sidebar-actions flex items-center space-x-3">
|
||||
<button id="open-search" class="text-gray-400 hover:text-white transition-colors" title="Search" aria-label="Search">
|
||||
<i class="fas fa-magnifying-glass"></i>
|
||||
</button>
|
||||
<a href="/settings" class="text-gray-400 hover:text-white transition-colors" title="Settings">
|
||||
<i class="fas fa-cog"></i>
|
||||
</a>
|
||||
@@ -334,6 +337,26 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search Modal -->
|
||||
<div id="search-modal" class="modal-overlay hidden">
|
||||
<div class="modal-content w-full max-w-3xl">
|
||||
<div class="flex items-center mb-3">
|
||||
<div class="relative flex-1">
|
||||
<i class="fas fa-magnifying-glass absolute left-3 top-3 text-gray-400"></i>
|
||||
<input id="search-input" type="text" placeholder="Search notes..." class="form-input pl-9 w-full" />
|
||||
</div>
|
||||
<button id="clear-search" class="ml-2 px-3 py-2 rounded bg-red-600 hover:bg-red-700 text-white" title="Clear">
|
||||
Clear
|
||||
</button>
|
||||
<button id="close-search" class="btn-secondary ml-2" title="Close">
|
||||
<i class="fas fa-xmark"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div id="search-status" class="text-sm text-gray-400 mb-2"></div>
|
||||
<div id="search-results" class="space-y-3"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scripts -->
|
||||
<script>
|
||||
// Initialize syntax highlighting
|
||||
@@ -441,6 +464,118 @@
|
||||
|
||||
// Expand active path in tree
|
||||
expandActivePath();
|
||||
|
||||
// Search modal wiring
|
||||
const searchModal = document.getElementById('search-modal');
|
||||
const openSearchBtn = document.getElementById('open-search');
|
||||
const closeSearchBtn = document.getElementById('close-search');
|
||||
const clearSearchBtn = document.getElementById('clear-search');
|
||||
const searchInput = document.getElementById('search-input');
|
||||
const searchResults = document.getElementById('search-results');
|
||||
const searchStatus = document.getElementById('search-status');
|
||||
|
||||
const LS_KEY_QUERY = 'globalSearch:query';
|
||||
|
||||
function openSearch() {
|
||||
if (!searchModal) return;
|
||||
searchModal.classList.remove('hidden');
|
||||
setTimeout(() => searchInput && searchInput.focus(), 0);
|
||||
const lastQuery = localStorage.getItem(LS_KEY_QUERY) || '';
|
||||
if (searchInput) {
|
||||
searchInput.value = lastQuery;
|
||||
if (lastQuery) doSearch(lastQuery);
|
||||
}
|
||||
}
|
||||
function closeSearch() {
|
||||
if (!searchModal) return;
|
||||
searchModal.classList.add('hidden');
|
||||
}
|
||||
function clearSearch() {
|
||||
if (!searchInput) return;
|
||||
searchInput.value = '';
|
||||
localStorage.removeItem(LS_KEY_QUERY);
|
||||
renderResults({ query: '', results: [] });
|
||||
}
|
||||
|
||||
let debounceTimer = null;
|
||||
function debounce(fn, delay) {
|
||||
return function(...args) {
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(() => fn.apply(this, args), delay);
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHTML(str) {
|
||||
return str.replace(/[&<>"']/g, s => ({'&':'&','<':'<','>':'>','"':'"','\'':'''}[s]));
|
||||
}
|
||||
|
||||
function renderResults(data) {
|
||||
if (!searchResults || !searchStatus) return;
|
||||
searchResults.innerHTML = '';
|
||||
if (!data || !data.results) {
|
||||
searchStatus.textContent = '';
|
||||
return;
|
||||
}
|
||||
searchStatus.textContent = `${data.results.length} result(s)`;
|
||||
data.results.forEach(r => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'p-3 bg-slate-700 rounded border border-slate-600';
|
||||
const title = document.createElement('div');
|
||||
title.className = 'flex items-center justify-between text-sm text-blue-300 hover:text-blue-200 cursor-pointer';
|
||||
title.innerHTML = `<span><i class="fas ${r.type === 'md' ? 'fa-file-lines' : 'fa-file'} mr-2"></i>${escapeHTML(r.path)}</span>`;
|
||||
title.addEventListener('click', () => {
|
||||
const url = r.path.endsWith('.md') ? `/note/${r.path}` : `/view_text/${r.path}`;
|
||||
// remember query
|
||||
if (searchInput) localStorage.setItem(LS_KEY_QUERY, searchInput.value.trim());
|
||||
window.location.href = url;
|
||||
});
|
||||
item.appendChild(title);
|
||||
|
||||
(r.snippets || []).forEach(snip => {
|
||||
const pre = document.createElement('pre');
|
||||
pre.className = 'mt-2 bg-slate-800 p-2 rounded text-xs whitespace-pre-wrap border border-slate-600';
|
||||
pre.textContent = `Line ${snip.line}:\n` + snip.preview;
|
||||
item.appendChild(pre);
|
||||
});
|
||||
searchResults.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
const doSearch = debounce(async function(q) {
|
||||
const query = (q || '').trim();
|
||||
if (!query) {
|
||||
renderResults({ query: '', results: [] });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
searchStatus.textContent = 'Searching...';
|
||||
const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
localStorage.setItem(LS_KEY_QUERY, query);
|
||||
renderResults(data);
|
||||
} else {
|
||||
searchStatus.textContent = data.error || 'Search failed';
|
||||
}
|
||||
} catch (e) {
|
||||
searchStatus.textContent = 'Search failed';
|
||||
}
|
||||
}, 250);
|
||||
|
||||
if (openSearchBtn) openSearchBtn.addEventListener('click', openSearch);
|
||||
if (closeSearchBtn) closeSearchBtn.addEventListener('click', closeSearch);
|
||||
if (clearSearchBtn) clearSearchBtn.addEventListener('click', clearSearch);
|
||||
if (searchInput) searchInput.addEventListener('input', (e) => doSearch(e.target.value));
|
||||
|
||||
// Close modal on overlay click (but not when clicking inside content)
|
||||
if (searchModal) {
|
||||
searchModal.addEventListener('click', (e) => {
|
||||
if (e.target === searchModal) closeSearch();
|
||||
});
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && !searchModal.classList.contains('hidden')) closeSearch();
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user