Add better search with debounce

This commit is contained in:
rdavis 2026-02-20 01:26:52 -06:00
parent 7073904143
commit e0b6fd06e4
3 changed files with 40 additions and 3 deletions

View File

@ -3,6 +3,7 @@
// Configuration constants
const CONFIG = {
AUTOSAVE_DELAY: 1000, // ms - Delay before triggering autosave
SEARCH_DEBOUNCE_DELAY: 1000, // ms - Delay before running note search while typing
SAVE_INDICATOR_DURATION: 2000, // ms - How long to show "saved" indicator
SCROLL_SYNC_DELAY: 50, // ms - Delay to prevent scroll sync interference
SCROLL_SYNC_MAX_RETRIES: 10, // Maximum attempts to find editor/preview elements
@ -223,6 +224,10 @@ function noteApp() {
selectedTags: [],
tagsExpanded: false,
tagReloadTimeout: null, // For debouncing tag reloads
// Search state
searchDebounceTimeout: null,
isSearching: false,
// Outline (TOC) state
outline: [], // [{level: 1, text: 'Heading', slug: 'heading'}, ...]
@ -1418,6 +1423,7 @@ function noteApp() {
// Case 1: No filters at all → show full folder tree
if (!hasTextSearch && !hasTagFilter) {
this.isSearching = false;
this.searchResults = [];
this.currentSearchHighlight = '';
this.clearSearchHighlights();
@ -1427,6 +1433,7 @@ function noteApp() {
// Case 2: Only tag filter → convert to flat list of matching notes
if (hasTagFilter && !hasTextSearch) {
this.isSearching = false;
this.searchResults = this.notes.filter(note =>
note.type === 'note' && this.noteMatchesTags(note)
);
@ -1437,6 +1444,7 @@ function noteApp() {
// Case 3: Text search (with or without tag filter)
if (hasTextSearch) {
this.isSearching = true;
try {
const response = await fetch(`/api/search?q=${encodeURIComponent(this.searchQuery)}`);
const data = await response.json();
@ -1461,6 +1469,9 @@ function noteApp() {
}
} catch (error) {
console.error('Search failed:', error);
this.searchResults = [];
} finally {
this.isSearching = false;
}
}
},
@ -3831,6 +3842,26 @@ function noteApp() {
},
// Search notes
debouncedSearchNotes() {
if (this.searchDebounceTimeout) {
clearTimeout(this.searchDebounceTimeout);
}
const hasTextSearch = this.searchQuery.trim().length > 0;
if (!hasTextSearch) {
this.isSearching = false;
this.searchNotes();
return;
}
this.isSearching = true;
this.searchResults = [];
this.searchDebounceTimeout = setTimeout(() => {
this.searchNotes();
}, CONFIG.SEARCH_DEBOUNCE_DELAY);
},
// Search notes by text (calls unified filter logic)
async searchNotes() {
await this.applyFilters();

View File

@ -1617,7 +1617,7 @@
<input
type="text"
x-model="searchQuery"
@input="searchNotes()"
@input="debouncedSearchNotes()"
:placeholder="t('sidebar.search_placeholder')"
class="w-full px-2 py-1.5 pl-7 pr-7 text-xs rounded focus:outline-none focus:ring-1"
style="background-color: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--border-primary);"
@ -1676,7 +1676,12 @@
</template>
</div>
</template>
<template x-if="searchQuery.trim() && searchResults.length === 0">
<template x-if="searchQuery.trim() && isSearching">
<div class="p-6 text-center">
<p class="text-sm" style="color: var(--text-tertiary);" x-text="t('search.searching', {query: searchQuery})"></p>
</div>
</template>
<template x-if="searchQuery.trim() && !isSearching && searchResults.length === 0">
<div class="p-6 text-center">
<svg class="w-12 h-12 mx-auto mb-3 opacity-40" style="color: var(--text-tertiary);" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>

View File

@ -217,7 +217,8 @@
"match_of": "Match {{current}} of {{total}}",
"result_singular": "result",
"result_plural": "results",
"no_results": "No notes contain \"{{query}}\""
"no_results": "No notes contain \"{{query}}\"",
"searching": "Searching for \"{{query}}\"..."
},
"theme": {