From 0fc2c9f26308f9f5caa5451a75ca634c8de586e8 Mon Sep 17 00:00:00 2001 From: Jin <22962980+JYC333@users.noreply.github.com> Date: Mon, 9 Mar 2026 23:30:36 +0000 Subject: [PATCH] refactor: remove old autocomplete declare --- .agents/migration_plan_autocomplete.md | 10 ++- .../src/services/attribute_autocomplete.ts | 4 +- apps/client/src/services/autocomplete_core.ts | 17 +++-- apps/client/src/services/note_autocomplete.ts | 3 +- apps/client/src/types.d.ts | 28 -------- .../client/src/widgets/PromotedAttributes.tsx | 65 ++++++------------- 6 files changed, 42 insertions(+), 85 deletions(-) diff --git a/.agents/migration_plan_autocomplete.md b/.agents/migration_plan_autocomplete.md index bff393f920..f1baa4fa36 100644 --- a/.agents/migration_plan_autocomplete.md +++ b/.agents/migration_plan_autocomplete.md @@ -232,13 +232,21 @@ --- -### Step 8: 更新类型声明 +### Step 8: 更新类型声明 ✅ 完成 **文件变更:** - `apps/client/src/types.d.ts` — 移除 `AutoCompleteConfig`、`AutoCompleteArg`、jQuery `.autocomplete()` 方法 +- `apps/client/src/widgets/PromotedAttributes.tsx` — 移除最后残留的 `$input.autocomplete(...)` 调用,改为复用 `attribute_autocomplete.ts` 的 headless label value autocomplete +- `apps/client/src/services/autocomplete_core.ts` — 收紧 headless source 默认类型,补齐 internal source 所需默认钩子 +- `apps/client/src/services/note_autocomplete.ts` — 移除对不存在的 `autocomplete.destroy()` 调用,清理类型不兼容点 **验证方式:** - TypeScript 编译无错误 +**当前完成情况:** +- ✅ `types.d.ts` 中遗留的 `AutoCompleteConfig`、`AutoCompleteArg` 与 jQuery `.autocomplete()` 扩展声明已删除。 +- ✅ `PromotedAttributes.tsx` 不再依赖旧版 `autocomplete.js` 类型或初始化流程,至此 client 代码中已无 `.autocomplete(...)` 调用残留。 +- ✅ 运行 `pnpm exec tsc -p apps/client/tsconfig.app.json --noEmit` 通过。 + --- ### Step 9: 移除旧库和 Polyfill diff --git a/apps/client/src/services/attribute_autocomplete.ts b/apps/client/src/services/attribute_autocomplete.ts index 9a86eb594c..7758862ba7 100644 --- a/apps/client/src/services/attribute_autocomplete.ts +++ b/apps/client/src/services/attribute_autocomplete.ts @@ -117,7 +117,7 @@ function initAttributeNameAutocomplete({ $el, attributeType, open, onValueChange getSources({ query }) { return [ - withHeadlessSourceDefaults({ + withHeadlessSourceDefaults({ sourceId: "attribute-names", getItems() { const type = typeof attributeType === "function" ? attributeType() : attributeType; @@ -303,7 +303,7 @@ function initLabelValueAutocomplete({ $el, open, nameCallback, onValueChange }: getSources({ query }) { return [ - withHeadlessSourceDefaults({ + withHeadlessSourceDefaults({ sourceId: "attribute-values", async getItems() { const attributeName = nameCallback ? nameCallback() : ""; diff --git a/apps/client/src/services/autocomplete_core.ts b/apps/client/src/services/autocomplete_core.ts index f06227cece..f35c45d2f4 100644 --- a/apps/client/src/services/autocomplete_core.ts +++ b/apps/client/src/services/autocomplete_core.ts @@ -2,11 +2,13 @@ import type { AutocompleteApi, AutocompleteSource, BaseItem } from "@algolia/aut export const HEADLESS_AUTOCOMPLETE_PANEL_SELECTOR = ".aa-core-panel"; +type HeadlessSourceDefaults = Required, "getItemUrl" | "onActive" | "onResolve">>; + const headlessAutocompleteClosers = new Set<() => void>(); -export function withHeadlessSourceDefaults( - source: AutocompleteSource -): AutocompleteSource { +export function withHeadlessSourceDefaults>( + source: TSource +): TSource & HeadlessSourceDefaults { return { getItemUrl() { return undefined; @@ -14,8 +16,11 @@ export function withHeadlessSourceDefaults( onActive() { // Headless consumers handle highlight side effects themselves. }, + onResolve() { + // Headless consumers resolve and render items manually. + }, ...source - }; + } as TSource & HeadlessSourceDefaults; } export function registerHeadlessAutocompleteCloser(close: () => void) { @@ -171,8 +176,8 @@ export function bindAutocompleteInput({ }, { type: "keydown", - listener: (event: KeyboardEvent) => { - onKeyDown?.(event, handlers); + listener: (event: Event) => { + onKeyDown?.(event as KeyboardEvent, handlers); } }, ...extraBindings diff --git a/apps/client/src/services/note_autocomplete.ts b/apps/client/src/services/note_autocomplete.ts index 8996a50e67..5acc34792c 100644 --- a/apps/client/src/services/note_autocomplete.ts +++ b/apps/client/src/services/note_autocomplete.ts @@ -190,7 +190,7 @@ function renderSuggestion(item: Suggestion): string { } function createSuggestionSource(options: Options, onSelectItem: (item: Suggestion) => void) { - return withHeadlessSourceDefaults({ + return withHeadlessSourceDefaults({ sourceId: "note-suggestions", async getItems({ query }: { query: string }) { return await fetchResolvedSuggestions(query, options); @@ -776,7 +776,6 @@ function initNoteAutocomplete($el: JQuery, options?: Options) { const cleanup = () => { unregisterGlobalCloser(); cleanupInputBindings(); - autocomplete.destroy(); panelController.destroy(); }; diff --git a/apps/client/src/types.d.ts b/apps/client/src/types.d.ts index f7673901c1..85ed355b5c 100644 --- a/apps/client/src/types.d.ts +++ b/apps/client/src/types.d.ts @@ -6,7 +6,6 @@ import type { PrintReport } from "./print"; import type { lint } from "./services/eslint"; import type { Froca } from "./services/froca-interface"; import { Library } from "./services/library_loader"; -import { Suggestion } from "./services/note_autocomplete"; import server from "./services/server"; import utils from "./services/utils"; @@ -83,34 +82,7 @@ declare global { "note-load-progress": CustomEvent<{ progress: number }>; } - interface AutoCompleteConfig { - appendTo?: HTMLElement | null; - hint?: boolean; - openOnFocus?: boolean; - minLength?: number; - tabAutocomplete?: boolean; - autoselect?: boolean; - dropdownMenuContainer?: HTMLElement; - debug?: boolean; - } - - type AutoCompleteCallback = (values: AutoCompleteArg[]) => void; - - interface AutoCompleteArg { - name?: string; - value?: string; - notePathTitle?: string; - displayKey?: "name" | "value" | "notePathTitle"; - cache?: boolean; - source?: (term: string, cb: AutoCompleteCallback) => void, - templates?: { - suggestion: (suggestion: Suggestion) => string | undefined - } - } - interface JQuery { - autocomplete: (action?: "close" | "open" | "destroy" | "val" | AutoCompleteConfig, args?: AutoCompleteArg[] | string) => JQuery; - getSelectedNotePath(): string | undefined; getSelectedNoteId(): string | null; setSelectedNotePath(notePath: string | null | undefined); diff --git a/apps/client/src/widgets/PromotedAttributes.tsx b/apps/client/src/widgets/PromotedAttributes.tsx index 1e0bdeb474..2fbfa706b8 100644 --- a/apps/client/src/widgets/PromotedAttributes.tsx +++ b/apps/client/src/widgets/PromotedAttributes.tsx @@ -8,6 +8,7 @@ import { Dispatch, StateUpdater, useCallback, useEffect, useRef, useState } from import NoteContext from "../components/note_context"; import FAttribute from "../entities/fattribute"; import FNote from "../entities/fnote"; +import attributeAutocompleteService from "../services/attribute_autocomplete"; import { Attribute } from "../services/attribute_parser"; import attributes from "../services/attributes"; import { t } from "../services/i18n"; @@ -36,7 +37,7 @@ interface CellProps { setCellToFocus(cell: Cell): void; } -type OnChangeEventData = TargetedEvent | InputEvent | JQuery.TriggeredEvent; +type OnChangeEventData = TargetedEvent | InputEvent; type OnChangeListener = (e: OnChangeEventData) => void | Promise; export default function PromotedAttributes() { @@ -200,11 +201,7 @@ function LabelInput(props: CellProps & { inputId: string }) { }, [ cell, componentId, note, setCells ]); const extraInputProps: InputHTMLAttributes = {}; - useTextLabelAutocomplete(inputId, valueAttr, definition, (e) => { - if (e.currentTarget instanceof HTMLInputElement) { - setDraft(e.currentTarget.value); - } - }); + useTextLabelAutocomplete(inputId, valueAttr, definition, setDraft); // React to model changes. useEffect(() => { @@ -413,55 +410,31 @@ function InputButton({ icon, className, title, onClick }: { ); } -function useTextLabelAutocomplete(inputId: string, valueAttr: Attribute, definition: DefinitionObject, onChangeListener: OnChangeListener) { - const [ attributeValues, setAttributeValues ] = useState<{ value: string }[] | null>(null); - - // Obtain data. +function useTextLabelAutocomplete(inputId: string, valueAttr: Attribute, definition: DefinitionObject, onValueChange: (value: string) => void) { useEffect(() => { if (definition.labelType !== "text") { return; } - server.get(`attribute-values/${encodeURIComponent(valueAttr.name)}`).then((_attributesValues) => { - setAttributeValues(_attributesValues.map((attribute) => ({ value: attribute }))); - }); - }, [ definition.labelType, valueAttr.name ]); - - // Initialize autocomplete. - useEffect(() => { - if (attributeValues?.length === 0) return; const el = document.getElementById(inputId) as HTMLInputElement | null; - if (!el) return; + if (!el) { + return; + } const $input = $(el); - $input.autocomplete( - { - appendTo: document.querySelector("body"), - hint: false, - autoselect: false, - openOnFocus: true, - minLength: 0, - tabAutocomplete: false - }, - [ - { - displayKey: "value", - source (term, cb) { - term = term.toLowerCase(); + attributeAutocompleteService.initLabelValueAutocomplete({ + $el: $input, + open: false, + nameCallback: () => valueAttr.name, + onValueChange: (value) => { + onValueChange(value); + } + }); - const filtered = (attributeValues ?? []).filter((attr) => attr.value.toLowerCase().includes(term)); - - cb(filtered); - } - } - ] - ); - - $input.off("autocomplete:selected"); - $input.on("autocomplete:selected", onChangeListener); - - return () => $input.autocomplete("destroy"); - }, [ inputId, attributeValues, onChangeListener ]); + return () => { + attributeAutocompleteService.destroyAutocomplete($input); + }; + }, [ definition.labelType, inputId, onValueChange, valueAttr.name ]); } async function updateAttribute(note: FNote, cell: Cell, componentId: string, value: string | undefined, setCells: Dispatch>) {