refactor: remove old autocomplete declare

This commit is contained in:
Jin 2026-03-09 23:30:36 +00:00
parent 249fec6191
commit 0fc2c9f263
6 changed files with 42 additions and 85 deletions

View File

@ -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

View File

@ -117,7 +117,7 @@ function initAttributeNameAutocomplete({ $el, attributeType, open, onValueChange
getSources({ query }) {
return [
withHeadlessSourceDefaults<NameItem>({
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<NameItem>({
withHeadlessSourceDefaults({
sourceId: "attribute-values",
async getItems() {
const attributeName = nameCallback ? nameCallback() : "";

View File

@ -2,11 +2,13 @@ import type { AutocompleteApi, AutocompleteSource, BaseItem } from "@algolia/aut
export const HEADLESS_AUTOCOMPLETE_PANEL_SELECTOR = ".aa-core-panel";
type HeadlessSourceDefaults = Required<Pick<AutocompleteSource<any>, "getItemUrl" | "onActive" | "onResolve">>;
const headlessAutocompleteClosers = new Set<() => void>();
export function withHeadlessSourceDefaults<TItem extends BaseItem>(
source: AutocompleteSource<TItem>
): AutocompleteSource<TItem> {
export function withHeadlessSourceDefaults<TSource extends AutocompleteSource<any>>(
source: TSource
): TSource & HeadlessSourceDefaults {
return {
getItemUrl() {
return undefined;
@ -14,8 +16,11 @@ export function withHeadlessSourceDefaults<TItem extends BaseItem>(
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<TItem extends BaseItem>({
},
{
type: "keydown",
listener: (event: KeyboardEvent) => {
onKeyDown?.(event, handlers);
listener: (event: Event) => {
onKeyDown?.(event as KeyboardEvent, handlers);
}
},
...extraBindings

View File

@ -190,7 +190,7 @@ function renderSuggestion(item: Suggestion): string {
}
function createSuggestionSource(options: Options, onSelectItem: (item: Suggestion) => void) {
return withHeadlessSourceDefaults<Suggestion>({
return withHeadlessSourceDefaults({
sourceId: "note-suggestions",
async getItems({ query }: { query: string }) {
return await fetchResolvedSuggestions(query, options);
@ -776,7 +776,6 @@ function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) {
const cleanup = () => {
unregisterGlobalCloser();
cleanupInputBindings();
autocomplete.destroy();
panelController.destroy();
};

View File

@ -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<HTMLElement>;
getSelectedNotePath(): string | undefined;
getSelectedNoteId(): string | null;
setSelectedNotePath(notePath: string | null | undefined);

View File

@ -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<HTMLInputElement, Event> | InputEvent | JQuery.TriggeredEvent<HTMLInputElement, undefined, HTMLInputElement, HTMLInputElement>;
type OnChangeEventData = TargetedEvent<HTMLInputElement, Event> | InputEvent;
type OnChangeListener = (e: OnChangeEventData) => void | Promise<void>;
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<string[]>(`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<StateUpdater<Cell[] | undefined>>) {