Merge pull request #173 from ricky-davis/feat/better-search

feat: debounce search input
This commit is contained in:
Guillermo Villar 2026-02-23 17:46:46 +01:00 committed by GitHub
commit 3495c0d9ee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 60 additions and 13 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
@ -227,6 +228,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'}, ...]
@ -1431,6 +1436,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();
@ -1440,6 +1446,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)
);
@ -1450,6 +1457,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();
@ -1474,6 +1482,9 @@ function noteApp() {
}
} catch (error) {
console.error('Search failed:', error);
this.searchResults = [];
} finally {
this.isSearching = false;
}
}
},
@ -3842,6 +3853,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

@ -1621,7 +1621,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);"
@ -1680,7 +1680,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": "Treffer {{current}} von {{total}}",
"result_singular": "Ergebnis",
"result_plural": "Ergebnisse",
"no_results": "Keine Notizen enthalten \"{{query}}\""
"no_results": "Keine Notizen enthalten \"{{query}}\"",
"searching": "Suche nach \"{{query}}\"..."
},
"theme": {

View File

@ -216,7 +216,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": {

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": {

View File

@ -217,7 +217,8 @@
"match_of": "Coincidencia {{current}} de {{total}}",
"result_singular": "resultado",
"result_plural": "resultados",
"no_results": "Ninguna nota contiene \"{{query}}\""
"no_results": "Ninguna nota contiene \"{{query}}\"",
"searching": "Buscando \"{{query}}\"..."
},
"theme": {

View File

@ -217,7 +217,8 @@
"match_of": "Résultat {{current}} sur {{total}}",
"result_singular": "résultat",
"result_plural": "résultats",
"no_results": "Aucune note ne contient \"{{query}}\""
"no_results": "Aucune note ne contient \"{{query}}\"",
"searching": "Recherche de \"{{query}}\"..."
},
"theme": {

View File

@ -217,7 +217,8 @@
"match_of": "Megfelelő {{current}} / {{total}}",
"result_singular": "eredmény",
"result_plural": "eredmények",
"no_results": "Nincs jegyzet amely tartalmazza a következőt: \"{{query}}\""
"no_results": "Nincs jegyzet amely tartalmazza a következőt: \"{{query}}\"",
"searching": "\"{{query}}\" keresése..."
},
"theme": {

View File

@ -216,7 +216,8 @@
"match_of": "Risultato {{current}} di {{total}}",
"result_singular": "risultato",
"result_plural": "risultati",
"no_results": "Nessuna nota contiene \"{{query}}\""
"no_results": "Nessuna nota contiene \"{{query}}\"",
"searching": "Ricerca di \"{{query}}\"..."
},
"theme": {

View File

@ -216,7 +216,8 @@
"match_of": "{{total}}件中{{current}}件目",
"result_singular": "件",
"result_plural": "件",
"no_results": "「{{query}}」を含むノートはありません"
"no_results": "「{{query}}」を含むノートはありません",
"searching": "「{{query}}」を検索中..."
},
"theme": {

View File

@ -216,7 +216,8 @@
"match_of": "Результат {{current}} из {{total}}",
"result_singular": "результат",
"result_plural": "результатов",
"no_results": "Нет заметок с \"{{query}}\""
"no_results": "Нет заметок с \"{{query}}\"",
"searching": "Поиск \"{{query}}\"..."
},
"theme": {

View File

@ -216,7 +216,8 @@
"match_of": "Zadetek {{current}} od {{total}}",
"result_singular": "rezultat",
"result_plural": "rezultatov",
"no_results": "Nobena beležka ne vsebuje \"{{query}}\""
"no_results": "Nobena beležka ne vsebuje \"{{query}}\"",
"searching": "Iskanje \"{{query}}\"..."
},
"theme": {

View File

@ -216,7 +216,8 @@
"match_of": "第 {{current}} 个,共 {{total}} 个",
"result_singular": "个结果",
"result_plural": "个结果",
"no_results": "没有笔记包含\"{{query}}\""
"no_results": "没有笔记包含\"{{query}}\"",
"searching": "正在搜索\"{{query}}\"..."
},
"theme": {