Merge pull request #173 from ricky-davis/feat/better-search
feat: debounce search input
This commit is contained in:
commit
3495c0d9ee
|
|
@ -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
|
||||||
|
|
@ -227,6 +228,10 @@ function noteApp() {
|
||||||
selectedTags: [],
|
selectedTags: [],
|
||||||
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'}, ...]
|
||||||
|
|
@ -1431,6 +1436,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();
|
||||||
|
|
@ -1440,6 +1446,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)
|
||||||
);
|
);
|
||||||
|
|
@ -1450,6 +1457,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();
|
||||||
|
|
@ -1474,6 +1482,9 @@ function noteApp() {
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Search failed:', error);
|
console.error('Search failed:', error);
|
||||||
|
this.searchResults = [];
|
||||||
|
} finally {
|
||||||
|
this.isSearching = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -3842,6 +3853,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();
|
||||||
|
|
|
||||||
|
|
@ -1621,7 +1621,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);"
|
||||||
|
|
@ -1680,7 +1680,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>
|
||||||
|
|
|
||||||
|
|
@ -217,7 +217,8 @@
|
||||||
"match_of": "Treffer {{current}} von {{total}}",
|
"match_of": "Treffer {{current}} von {{total}}",
|
||||||
"result_singular": "Ergebnis",
|
"result_singular": "Ergebnis",
|
||||||
"result_plural": "Ergebnisse",
|
"result_plural": "Ergebnisse",
|
||||||
"no_results": "Keine Notizen enthalten \"{{query}}\""
|
"no_results": "Keine Notizen enthalten \"{{query}}\"",
|
||||||
|
"searching": "Suche nach \"{{query}}\"..."
|
||||||
},
|
},
|
||||||
|
|
||||||
"theme": {
|
"theme": {
|
||||||
|
|
|
||||||
|
|
@ -216,7 +216,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": {
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -217,7 +217,8 @@
|
||||||
"match_of": "Coincidencia {{current}} de {{total}}",
|
"match_of": "Coincidencia {{current}} de {{total}}",
|
||||||
"result_singular": "resultado",
|
"result_singular": "resultado",
|
||||||
"result_plural": "resultados",
|
"result_plural": "resultados",
|
||||||
"no_results": "Ninguna nota contiene \"{{query}}\""
|
"no_results": "Ninguna nota contiene \"{{query}}\"",
|
||||||
|
"searching": "Buscando \"{{query}}\"..."
|
||||||
},
|
},
|
||||||
|
|
||||||
"theme": {
|
"theme": {
|
||||||
|
|
|
||||||
|
|
@ -217,7 +217,8 @@
|
||||||
"match_of": "Résultat {{current}} sur {{total}}",
|
"match_of": "Résultat {{current}} sur {{total}}",
|
||||||
"result_singular": "résultat",
|
"result_singular": "résultat",
|
||||||
"result_plural": "résultats",
|
"result_plural": "résultats",
|
||||||
"no_results": "Aucune note ne contient \"{{query}}\""
|
"no_results": "Aucune note ne contient \"{{query}}\"",
|
||||||
|
"searching": "Recherche de \"{{query}}\"..."
|
||||||
},
|
},
|
||||||
|
|
||||||
"theme": {
|
"theme": {
|
||||||
|
|
|
||||||
|
|
@ -217,7 +217,8 @@
|
||||||
"match_of": "Megfelelő {{current}} / {{total}}",
|
"match_of": "Megfelelő {{current}} / {{total}}",
|
||||||
"result_singular": "eredmény",
|
"result_singular": "eredmény",
|
||||||
"result_plural": "eredmények",
|
"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": {
|
"theme": {
|
||||||
|
|
|
||||||
|
|
@ -216,7 +216,8 @@
|
||||||
"match_of": "Risultato {{current}} di {{total}}",
|
"match_of": "Risultato {{current}} di {{total}}",
|
||||||
"result_singular": "risultato",
|
"result_singular": "risultato",
|
||||||
"result_plural": "risultati",
|
"result_plural": "risultati",
|
||||||
"no_results": "Nessuna nota contiene \"{{query}}\""
|
"no_results": "Nessuna nota contiene \"{{query}}\"",
|
||||||
|
"searching": "Ricerca di \"{{query}}\"..."
|
||||||
},
|
},
|
||||||
|
|
||||||
"theme": {
|
"theme": {
|
||||||
|
|
|
||||||
|
|
@ -216,7 +216,8 @@
|
||||||
"match_of": "{{total}}件中{{current}}件目",
|
"match_of": "{{total}}件中{{current}}件目",
|
||||||
"result_singular": "件",
|
"result_singular": "件",
|
||||||
"result_plural": "件",
|
"result_plural": "件",
|
||||||
"no_results": "「{{query}}」を含むノートはありません"
|
"no_results": "「{{query}}」を含むノートはありません",
|
||||||
|
"searching": "「{{query}}」を検索中..."
|
||||||
},
|
},
|
||||||
|
|
||||||
"theme": {
|
"theme": {
|
||||||
|
|
|
||||||
|
|
@ -216,7 +216,8 @@
|
||||||
"match_of": "Результат {{current}} из {{total}}",
|
"match_of": "Результат {{current}} из {{total}}",
|
||||||
"result_singular": "результат",
|
"result_singular": "результат",
|
||||||
"result_plural": "результатов",
|
"result_plural": "результатов",
|
||||||
"no_results": "Нет заметок с \"{{query}}\""
|
"no_results": "Нет заметок с \"{{query}}\"",
|
||||||
|
"searching": "Поиск \"{{query}}\"..."
|
||||||
},
|
},
|
||||||
|
|
||||||
"theme": {
|
"theme": {
|
||||||
|
|
|
||||||
|
|
@ -216,7 +216,8 @@
|
||||||
"match_of": "Zadetek {{current}} od {{total}}",
|
"match_of": "Zadetek {{current}} od {{total}}",
|
||||||
"result_singular": "rezultat",
|
"result_singular": "rezultat",
|
||||||
"result_plural": "rezultatov",
|
"result_plural": "rezultatov",
|
||||||
"no_results": "Nobena beležka ne vsebuje \"{{query}}\""
|
"no_results": "Nobena beležka ne vsebuje \"{{query}}\"",
|
||||||
|
"searching": "Iskanje \"{{query}}\"..."
|
||||||
},
|
},
|
||||||
|
|
||||||
"theme": {
|
"theme": {
|
||||||
|
|
|
||||||
|
|
@ -216,7 +216,8 @@
|
||||||
"match_of": "第 {{current}} 个,共 {{total}} 个",
|
"match_of": "第 {{current}} 个,共 {{total}} 个",
|
||||||
"result_singular": "个结果",
|
"result_singular": "个结果",
|
||||||
"result_plural": "个结果",
|
"result_plural": "个结果",
|
||||||
"no_results": "没有笔记包含\"{{query}}\""
|
"no_results": "没有笔记包含\"{{query}}\"",
|
||||||
|
"searching": "正在搜索\"{{query}}\"..."
|
||||||
},
|
},
|
||||||
|
|
||||||
"theme": {
|
"theme": {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue