diff --git a/frontend/src/components/TagInput.vue b/frontend/src/components/TagInput.vue index 9dc3b25..7fc8a1f 100644 --- a/frontend/src/components/TagInput.vue +++ b/frontend/src/components/TagInput.vue @@ -14,6 +14,7 @@ const inputValue = ref(""); const suggestions = ref([]); const showSuggestions = ref(false); const inputRef = ref(null); +const selectedIndex = ref(-1); let fetchDebounce: ReturnType | null = null; @@ -37,11 +38,23 @@ function removeTag(tag: string) { } function onKeydown(e: KeyboardEvent) { + if (showSuggestions.value && e.key === "ArrowDown") { + e.preventDefault(); + selectedIndex.value = Math.min(selectedIndex.value + 1, suggestions.value.length - 1); + return; + } + if (showSuggestions.value && e.key === "ArrowUp") { + e.preventDefault(); + selectedIndex.value = Math.max(selectedIndex.value - 1, -1); + return; + } if (e.key === "Enter" || e.key === ",") { e.preventDefault(); - const val = inputValue.value.replace(/,$/, ""); - if (val.trim()) { - addTag(val); + if (showSuggestions.value && selectedIndex.value >= 0) { + addTag(suggestions.value[selectedIndex.value]); + } else { + const val = inputValue.value.replace(/,$/, ""); + if (val.trim()) addTag(val); } } else if (e.key === "Backspace" && !inputValue.value) { const tags = props.modelValue; @@ -50,6 +63,7 @@ function onKeydown(e: KeyboardEvent) { } } else if (e.key === "Escape") { showSuggestions.value = false; + selectedIndex.value = -1; } } @@ -66,6 +80,7 @@ function onInput() { const results = await props.fetchTags(q); suggestions.value = results.filter((t) => !props.modelValue.includes(t)); showSuggestions.value = suggestions.value.length > 0; + selectedIndex.value = -1; } catch { suggestions.value = []; } @@ -120,9 +135,9 @@ function focusInput() { />
  • #{{ s }} @@ -205,7 +220,8 @@ function focusInput() { cursor: pointer; color: var(--color-text); } -.tag-autocomplete-item:hover { +.tag-autocomplete-item:hover, +.tag-autocomplete-item.selected { background: var(--color-bg-hover, color-mix(in srgb, var(--color-primary) 8%, transparent)); color: var(--color-primary); } diff --git a/frontend/src/components/TiptapEditor.vue b/frontend/src/components/TiptapEditor.vue index 5152bcf..f58acd3 100644 --- a/frontend/src/components/TiptapEditor.vue +++ b/frontend/src/components/TiptapEditor.vue @@ -24,6 +24,7 @@ const props = withDefaults( const emit = defineEmits<{ "update:modelValue": [value: string]; selectionChange: [payload: { text: string; start: number; end: number }]; + escape: []; }>(); let updatingFromProp = false; @@ -41,6 +42,14 @@ try { content: markdownToHtml(props.modelValue), editable: true, editorProps: { + handleKeyDown: (_view, event) => { + if (event.key === "Escape") { + editor?.commands.blur(); + emit("escape"); + return true; + } + return false; + }, handlePaste: (_view, event) => { const text = event.clipboardData?.getData("text/plain"); if (!text || !editor) return false; diff --git a/frontend/src/components/WorkspaceNoteEditor.vue b/frontend/src/components/WorkspaceNoteEditor.vue index f6513a7..6b61760 100644 --- a/frontend/src/components/WorkspaceNoteEditor.vue +++ b/frontend/src/components/WorkspaceNoteEditor.vue @@ -47,6 +47,7 @@ const saving = ref(false); const tagSuggestions = useTagSuggestions(noteTitle, noteBody, noteTags, () => { dirty.value = true; }); // TipTap editor ref (for MarkdownToolbar) +const titleRef = ref(null); const editorRef = ref | null>(null); const tiptapEditor = computed(() => (editorRef.value?.editor as Editor | undefined) ?? null @@ -252,10 +253,12 @@ onUnmounted(() => { if (linkCheckTimer) clearTimeout(linkCheckTimer); });
    @@ -308,8 +311,8 @@ onUnmounted(() => { if (linkCheckTimer) clearTimeout(linkCheckTimer); }); -
    - +
    +
    diff --git a/frontend/src/views/NoteEditorView.vue b/frontend/src/views/NoteEditorView.vue index 6103275..25b72ab 100644 --- a/frontend/src/views/NoteEditorView.vue +++ b/frontend/src/views/NoteEditorView.vue @@ -36,6 +36,7 @@ const showPreview = ref(false); const sidebarOpen = ref(true); const showHistory = ref(false); const editorRef = ref | null>(null); +const titleRef = ref(null); const tiptapEditor = computed(() => { return (editorRef.value?.editor as Editor | undefined) ?? null; }); @@ -325,11 +326,14 @@ onUnmounted(() => assist.clearSelection());
    @@ -337,7 +341,7 @@ onUnmounted(() => assist.clearSelection());
    -
    +
    @@ -366,6 +370,7 @@ onUnmounted(() => assist.clearSelection()); placeholder="Write your note in Markdown..." @update:modelValue="onBodyUpdate" @selectionChange="onSelectionChange" + @escape="titleRef?.focus()" />
    | null>(null); +const titleRef = ref(null); const tiptapEditor = computed(() => { return (editorRef.value?.editor as Editor | undefined) ?? null; }); @@ -392,8 +393,11 @@ useEditorGuards(dirty, save); v-model="title" type="text" placeholder="Task title" + ref="titleRef" class="title-input" @input="markDirty" + @keydown.ctrl.s.prevent="save" + @keydown.ctrl.e.prevent="tiptapEditor?.commands.focus()" />
    @@ -402,7 +406,7 @@ useEditorGuards(dirty, save);
    -
    +
    @@ -434,6 +438,7 @@ useEditorGuards(dirty, save); placeholder="Describe this task..." @update:modelValue="onBodyUpdate" @selectionChange="onSelectionChange" + @escape="titleRef?.focus()" />