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

View File

@ -1617,7 +1617,7 @@
<input <input
type="text" type="text"
x-model="searchQuery" x-model="searchQuery"
@input="searchNotes()" @input="debouncedSearchNotes()"
:placeholder="t('sidebar.search_placeholder')" :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" 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);" style="background-color: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--border-primary);"
@ -1676,7 +1676,12 @@
</template> </template>
</div> </div>
</template> </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"> <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"> <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> <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}}", "match_of": "Match {{current}} of {{total}}",
"result_singular": "result", "result_singular": "result",
"result_plural": "results", "result_plural": "results",
"no_results": "No notes contain \"{{query}}\"" "no_results": "No notes contain \"{{query}}\"",
"searching": "Searching for \"{{query}}\"..."
}, },
"theme": { "theme": {