From bd1f6b7a0f43810f9bf4c481c49d70bf51618256 Mon Sep 17 00:00:00 2001 From: JYC333 <22962980+JYC333@users.noreply.github.com> Date: Mon, 9 Mar 2026 15:44:55 +0000 Subject: [PATCH] refactor: fix full search --- .agents/migration_plan_autocomplete.md | 8 +- apps/client/src/services/note_autocomplete.ts | 115 +++++++++++++++--- 2 files changed, 108 insertions(+), 15 deletions(-) diff --git a/.agents/migration_plan_autocomplete.md b/.agents/migration_plan_autocomplete.md index 33f86d9792..f1e1ed8272 100644 --- a/.agents/migration_plan_autocomplete.md +++ b/.agents/migration_plan_autocomplete.md @@ -122,13 +122,19 @@ - ✅ `Ctrl+J / Jump to Note` 与 `attribute_detail.ts` 的普通 note 选择链路已抽查通过。 - ⚠️ React 消费方整体仍应放在 **Step 5** 继续验收;`Move to` 等问题不属于 Step 3.3 本身已完成的范围。 -#### Step 3.4: 特殊键盘事件拦截与附带按钮包容 (终极打磨) +#### Step 3.4: 特殊键盘事件拦截与附带按钮包容 (终极打磨) ✅ 基本完成 **目标:** 解决在旧 jQuery 中强绑定的 IME(中日韩等输入法)防抖问题,并恢复如 `Shift+Enter`、周边附加按钮(清除等)的正常运作。 **工作内容:** - 将旧的输入法合成事件 (`compositionstart` / `compositionend`) 判断逻辑迁移到新的 `onInput` / `onKeyDown` 外围保护之中。 - 重构对 `Shift+Enter` (唤起全文搜索)、`Ctrl+Enter` 等组合快捷键的劫持处理。 - 修正周边辅助控件(例如搜索栏自带的 “最近笔记(钟表)”、“清除栏(X)” 按钮)因为 DOM 结构调整可能引发的影响。 **验证方式:** 中文拼音输入法敲打途中不会错误地发起网络搜索;各种组合回车热键重新生效,整个搜索系统重回巅峰状态。 +**当前验证结果:** +- ✅ `compositionstart` / `compositionend` 已恢复旧版保护逻辑:合成期间不发起搜索,结束后按“清空再恢复 query”的语义重新跑一次。 +- ✅ `Shift+Enter` 与 `Ctrl+Enter` 的快捷分发仍保留,并已按旧版语义接回全文搜索 / `search-notes`。 +- ✅ `autocomplete:opened` / `autocomplete:closed` 事件已重新补回,`readonly` 与“关闭时空输入框清理”逻辑重新对齐旧版。 +- ✅ 清空按钮、最近笔记按钮、全文搜索按钮都继续走 service 内部统一入口,而不是分散拼状态。 +- ⚠️ 这一步仍以 `note_autocomplete.ts` 核心行为为主;React 消费方的问题继续留在 **Step 5**。 --- diff --git a/apps/client/src/services/note_autocomplete.ts b/apps/client/src/services/note_autocomplete.ts index 7e36fa8647..f030670b17 100644 --- a/apps/client/src/services/note_autocomplete.ts +++ b/apps/client/src/services/note_autocomplete.ts @@ -62,6 +62,9 @@ interface ManagedInstance { autocomplete: CoreAutocompleteApi; panelEl: HTMLElement; clearCursor: () => void; + isPanelOpen: () => boolean; + getQuery: () => string; + suppressNextClosedReset: () => void; showQuery: (query: string) => void; openRecentNotes: () => void; cleanup: () => void; @@ -261,7 +264,7 @@ function renderItems( } async function autocompleteSourceForCKEditor(queryText: string) { - const rows = await fetchSuggestions(queryText, { allowCreatingNotes: true }); + const rows = await fetchResolvedSuggestions(queryText, { allowCreatingNotes: true }); return rows.map((row) => { return { action: row.action, @@ -275,7 +278,20 @@ async function autocompleteSourceForCKEditor(queryText: string) { }); } -async function fetchSuggestions(term: string, options: Options = {}): Promise { +function getSearchingSuggestion(term: string): Suggestion[] { + if (term.trim().length === 0) { + return []; + } + + return [ + { + noteTitle: term, + highlightedNotePathTitle: t("quick-search.searching") + } + ]; +} + +async function fetchResolvedSuggestions(term: string, options: Options = {}): Promise { // Check if we're in command mode if (options.isCommandPalette && term.startsWith(">")) { const commandQuery = term.substring(1).trim(); @@ -305,12 +321,6 @@ async function fetchSuggestions(term: string, options: Options = {}): Promise((resolve) => { clearTimeout(debounceTimeoutId); debounceTimeoutId = setTimeout(async () => { - resolve(await fetchSuggestions(term, options)); + resolve(await fetchResolvedSuggestions(term, options)); }, searchDelay); if (searchDelay === 0) { @@ -435,6 +445,9 @@ function clearText($el: JQuery) { const inputEl = $el[0] as HTMLInputElement; const instance = getManagedInstance($el); if (instance) { + if (instance.isPanelOpen()) { + instance.suppressNextClosedReset(); + } inputEl.value = ""; instance.clearCursor(); instance.autocomplete.setQuery(""); @@ -480,9 +493,13 @@ function fullTextSearch($el: JQuery, options: Options) { $el.trigger("focus"); options.fastSearch = false; searchDelay = 0; + resetSelectionState($el); const instance = getManagedInstance($el); if (instance) { + instance.clearCursor(); + instance.autocomplete.setQuery(""); + inputEl.value = ""; instance.showQuery(searchString); } } @@ -507,6 +524,9 @@ function initNoteAutocomplete($el: JQuery, options?: Options) { let currentQuery = inputEl.value; let shouldAutoselectTopItem = false; let shouldMirrorActiveItemToInput = false; + let wasPanelOpen = false; + let suppressNextClosedEmptyReset = false; + let suggestionRequestId = 0; const clearCursor = () => { shouldMirrorActiveItemToInput = false; @@ -514,11 +534,28 @@ function initNoteAutocomplete($el: JQuery, options?: Options) { inputEl.value = currentQuery; }; + const suppressNextClosedReset = () => { + suppressNextClosedEmptyReset = true; + }; + const prepareForQueryChange = () => { shouldAutoselectTopItem = true; shouldMirrorActiveItemToInput = false; }; + const rerunQuery = (query: string) => { + if (!query.trim().length) { + openRecentNotes(); + return; + } + + prepareForQueryChange(); + currentQuery = ""; + inputEl.value = ""; + autocomplete.setQuery(""); + showQuery(query); + }; + const onSelectItem = async (item: Suggestion) => { await handleSuggestionSelection($el, autocomplete, inputEl, item); }; @@ -539,9 +576,8 @@ function initNoteAutocomplete($el: JQuery, options?: Options) { inputEl.value = ""; autocomplete.setQuery(""); autocomplete.setActiveItemId(null); - autocomplete.setIsOpen(false); - fetchSuggestions("", options).then((items) => { + fetchResolvedSuggestions("", options).then((items) => { autocomplete.setCollections([{ source, items }]); autocomplete.setIsOpen(items.length > 0); }); @@ -582,6 +618,22 @@ function initNoteAutocomplete($el: JQuery, options?: Options) { if (isComposingInput) { return []; } + + if (options.fastSearch === false && query.trim().length > 0) { + const requestId = ++suggestionRequestId; + + void fetchSuggestionsWithDelay(query, options).then((items) => { + if (requestId !== suggestionRequestId || currentQuery !== query) { + return; + } + + autocomplete.setCollections([{ source, items }]); + autocomplete.setIsOpen(items.length > 0); + }); + + return getSearchingSuggestion(query); + } + return await fetchSuggestionsWithDelay(query, options); } }, @@ -594,6 +646,31 @@ function initNoteAutocomplete($el: JQuery, options?: Options) { const activeId = state.activeItemId ?? null; const activeItem = activeId !== null ? items[activeId] : null; currentQuery = state.query; + const isPanelOpen = state.isOpen && items.length > 0; + + if (isPanelOpen !== wasPanelOpen) { + wasPanelOpen = isPanelOpen; + + if (isPanelOpen) { + $el.trigger("autocomplete:opened"); + + if (inputEl.readOnly) { + suppressNextClosedReset(); + autocomplete.setIsOpen(false); + return; + } + } else { + $el.trigger("autocomplete:closed"); + + if (suppressNextClosedEmptyReset) { + suppressNextClosedEmptyReset = false; + } else if (!String(inputEl.value).trim()) { + searchDelay = 0; + resetSelectionState($el); + $el.trigger("change"); + } + } + } if (activeItem && shouldMirrorActiveItemToInput) { inputEl.value = getSuggestionInputValue(activeItem); @@ -601,7 +678,7 @@ function initNoteAutocomplete($el: JQuery, options?: Options) { inputEl.value = state.query; } - if (state.isOpen && items.length > 0) { + if (isPanelOpen) { renderItems(panelEl, items, activeId, (item) => { void onSelectItem(item); }, (index) => { @@ -684,7 +761,7 @@ function initNoteAutocomplete($el: JQuery, options?: Options) { }; const onCompositionEnd = (e: CompositionEvent) => { isComposingInput = false; - handlers.onChange(e as any); + rerunQuery(inputEl.value); }; inputEl.addEventListener("input", onInput); @@ -707,7 +784,17 @@ function initNoteAutocomplete($el: JQuery, options?: Options) { } }; - instanceMap.set(inputEl, { autocomplete, panelEl, clearCursor, showQuery, openRecentNotes, cleanup }); + instanceMap.set(inputEl, { + autocomplete, + panelEl, + clearCursor, + isPanelOpen: () => wasPanelOpen, + getQuery: () => currentQuery, + suppressNextClosedReset, + showQuery, + openRecentNotes, + cleanup + }); // Buttons UI logic const $clearTextButton = $("").addClass("input-group-text input-clearer-button bx bxs-tag-x").prop("title", t("note_autocomplete.clear-text-field"));