From 5defddb35c386dd5302cab672cf4a9a6eb1d1431 Mon Sep 17 00:00:00 2001 From: Yuri Karamian Date: Mon, 8 Jun 2026 12:51:03 +0200 Subject: [PATCH] Add tag autocomplete: suggest existing tags while typing to avoid near-duplicates --- src/lib/components/NoteList.svelte | 49 +++------ src/lib/components/TagSuggestInput.svelte | 128 ++++++++++++++++++++++ 2 files changed, 144 insertions(+), 33 deletions(-) create mode 100644 src/lib/components/TagSuggestInput.svelte diff --git a/src/lib/components/NoteList.svelte b/src/lib/components/NoteList.svelte index b544f6e..bf179f8 100644 --- a/src/lib/components/NoteList.svelte +++ b/src/lib/components/NoteList.svelte @@ -39,6 +39,7 @@ import { revealItemInDir } from '@tauri-apps/plugin-opener'; import type { NoteEntry, TrashNotebookEntry, SortMode, TaskItem } from '$lib/types'; import TasksView from './TasksView.svelte'; + import TagSuggestInput from './TagSuggestInput.svelte'; let { onNoteSelected = (_path: string, _content: string) => {}, onNoteMoved = () => {}, onBeforeNoteSwitch = () => {}, onNoteCreated = () => {}, onToggleTask = async (_t: TaskItem) => {}, onSetTaskPriority = async (_t: TaskItem, _p: string | null) => {}, onSetTaskDue = async (_t: TaskItem, _d: string | null) => {} }: { onNoteSelected?: (path: string, content: string) => void; @@ -131,7 +132,7 @@ if (isMobile) { deriveTagsFromNotes(); } else { - await refreshTags(); + try { $tags = await getAllTags(); } catch (_) {} } } @@ -143,7 +144,6 @@ let editValue = $state(''); let movePickerNote = $state(null); let tagEditNote = $state(null); - let tagEditValue = $state(''); let tagEditTags = $state([]); // Multi-select @@ -532,7 +532,7 @@ function openTagEdit(note: NoteEntry) { tagEditNote = note; tagEditTags = [...note.meta.tags]; - tagEditValue = ''; + refreshTags(); } async function addTagToNote(tag: string) { @@ -540,7 +540,6 @@ const cleaned = tag.trim().toLowerCase(); if (tagEditTags.includes(cleaned)) return; tagEditTags = [...tagEditTags, cleaned]; - tagEditValue = ''; await saveTagsForNote(tagEditNote, tagEditTags); } @@ -569,7 +568,7 @@ function openBatchTagEdit() { batchTagEdit = true; - tagEditValue = ''; + refreshTags(); // Show tags common to ALL selected notes const selectedNotes = $notes.filter(n => selectedPaths.has(n.path)); if (selectedNotes.length === 0) { tagEditTags = []; return; } @@ -581,7 +580,6 @@ async function addTagToBatch(tag: string) { if (!tag.trim()) return; const cleaned = tag.trim().toLowerCase(); - tagEditValue = ''; if (!tagEditTags.includes(cleaned)) tagEditTags = [...tagEditTags, cleaned]; try { for (const path of selectedPaths) { @@ -940,16 +938,11 @@
- { - if (e.key === 'Enter') { e.preventDefault(); addTagToBatch(tagEditValue); } - if (e.key === 'Escape') { e.preventDefault(); batchTagEdit = false; } - }} - autofocus + onsubmit={(t) => addTagToBatch(t)} + oncancel={() => batchTagEdit = false} />
{#if tagEditTags.length > 0} @@ -1208,16 +1201,11 @@
- { - if (e.key === 'Enter') { e.preventDefault(); addTagToBatch(tagEditValue); } - if (e.key === 'Escape') { e.preventDefault(); batchTagEdit = false; } - }} - autofocus + onsubmit={(t) => addTagToBatch(t)} + oncancel={() => batchTagEdit = false} />
{#if tagEditTags.length > 0} @@ -1265,16 +1253,11 @@
- { - if (e.key === 'Enter') { e.preventDefault(); addTagToNote(tagEditValue); } - if (e.key === 'Escape') { e.preventDefault(); tagEditNote = null; } - }} - autofocus + onsubmit={(t) => addTagToNote(t)} + oncancel={() => tagEditNote = null} />
{#if tagEditTags.length > 0} diff --git a/src/lib/components/TagSuggestInput.svelte b/src/lib/components/TagSuggestInput.svelte new file mode 100644 index 0000000..3467ee6 --- /dev/null +++ b/src/lib/components/TagSuggestInput.svelte @@ -0,0 +1,128 @@ + + +
+ (selIndex = -1)} + onkeydown={onKeydown} + /> + {#if suggestions.length} +
+ {#each suggestions as s, i} + + {/each} +
+ {/if} +
+ +