refactor: migrate react part

This commit is contained in:
JYC333 2026-03-09 16:50:35 +00:00
parent 3ac2e2785d
commit 0dee06262b
3 changed files with 106 additions and 56 deletions

View File

@ -164,7 +164,7 @@
---
### Step 5: 迁移 `NoteAutocomplete.tsx` (React/Preact 组件)
### Step 5: 迁移 `NoteAutocomplete.tsx` (React/Preact 组件) ✅ 基本完成
**文件变更:**
- `apps/client/src/widgets/react/NoteAutocomplete.tsx` — 传入容器 `<div>`,管理 `api` 生命周期
@ -173,8 +173,10 @@
- `noteId``text` props 的动态更新正确
**当前状态:**
- ⚠️ 尚未完成。虽然底层 `note_autocomplete.ts` 已经切到新实现,但 React 消费方仍需逐一验收。
- ⚠️ 已抽查 `Move to`,当前功能不正常,说明 Step 5 仍存在待修复问题。
- ✅ `NoteAutocomplete.tsx` 已移除残留的旧 `.autocomplete("val")` 调用,改为完全走 `note_autocomplete.ts` 暴露的 helper。
- ✅ 组件现在会显式管理 headless autocomplete 的初始化/销毁生命周期,并清理 React 侧追加的 DOM / jQuery 监听,避免重复绑定。
- ✅ `noteId` / `text` prop 同步已切到新状态流,`setNote()` 也会同步内部 query避免仅改 DOM 值导致的状态漂移。
- ⚠️ 仍需继续做手动回归验收,重点应覆盖 `Move to``Clone to``Include note``Add link`、bulk actions 等主要 React 消费方。
---

View File

@ -454,7 +454,7 @@ async function handleSuggestionSelection(
$el.trigger("autocomplete:noteselected", [suggestion]);
}
function clearText($el: JQuery<HTMLElement>) {
export function clearText($el: JQuery<HTMLElement>) {
searchDelay = 0;
resetSelectionState($el);
const inputEl = $el[0] as HTMLInputElement;
@ -594,6 +594,7 @@ function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) {
fetchResolvedSuggestions("", options).then((items) => {
autocomplete.setCollections([{ source, items }]);
autocomplete.setActiveItemId(items.length > 0 ? 0 : null);
autocomplete.setIsOpen(items.length > 0);
});
};
@ -620,7 +621,9 @@ function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) {
const autocomplete = createAutocomplete<Suggestion>({
openOnFocus: false, // Wait until we explicitly focus or type
defaultActiveItemId: null,
// Old autocomplete.js used `autoselect: true`, so the first item
// should be immediately selectable when the panel opens.
defaultActiveItemId: 0,
shouldPanelOpen() {
return true;
},
@ -794,6 +797,7 @@ function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) {
inputEl.removeEventListener("compositionstart", onCompositionStart);
inputEl.removeEventListener("compositionend", onCompositionEnd);
stopPositioning();
autocomplete.destroy();
if (panelEl.parentElement) {
panelEl.parentElement.removeChild(panelEl);
}
@ -836,6 +840,15 @@ function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) {
return $el;
}
export function destroyAutocomplete($el: JQuery<HTMLElement> | HTMLElement) {
const inputEl = $el instanceof HTMLElement ? $el : $el[0] as HTMLInputElement;
const instance = instanceMap.get(inputEl);
if (instance) {
instance.cleanup();
instanceMap.delete(inputEl);
}
}
function init() {
$.fn.getSelectedNotePath = function () {
if (!String($(this).val())?.trim()) {
@ -878,10 +891,19 @@ function init() {
$.fn.setNote = async function (noteId) {
const note = noteId ? await froca.getNote(noteId, true) : null;
const $el = $(this as unknown as HTMLElement);
const instance = getManagedInstance($el);
const noteTitle = note ? note.title : "";
$(this)
.val(note ? note.title : "")
$el
.val(noteTitle)
.setSelectedNotePath(noteId);
if (instance) {
instance.clearCursor();
instance.autocomplete.setQuery(noteTitle);
instance.autocomplete.setIsOpen(false);
}
};
}
@ -902,6 +924,8 @@ export function triggerRecentNotes(inputElement: HTMLInputElement | null | undef
export default {
autocompleteSourceForCKEditor,
clearText,
destroyAutocomplete,
initNoteAutocomplete,
showRecentNotes,
showAllCommands,

View File

@ -28,73 +28,97 @@ export default function NoteAutocomplete({ id, inputRef: externalInputRef, text,
if (!ref.current) return;
const $autoComplete = $(ref.current);
// clear any event listener added in previous invocation of this function
$autoComplete
.off("autocomplete:noteselected")
.off("autocomplete:commandselected")
note_autocomplete.initNoteAutocomplete($autoComplete, {
...opts,
container: container?.current
});
if (onTextChange) {
$autoComplete.on("input", () => onTextChange($autoComplete[0].value));
}
if (onKeyDown) {
$autoComplete.on("keydown", (e) => e.originalEvent && onKeyDown(e.originalEvent));
}
if (onBlur) {
$autoComplete.on("blur", () => onBlur($autoComplete.getSelectedNoteId() ?? ""));
}
}, [opts, container?.current]);
// On change event handlers.
useEffect(() => {
if (!ref.current) return;
const $autoComplete = $(ref.current);
const inputListener = () => onTextChange?.($autoComplete[0].value);
const keyDownListener = (e) => e.originalEvent && onKeyDown?.(e.originalEvent);
const blurListener = () => onBlur?.($autoComplete.getSelectedNoteId() ?? "");
if (onChange || noteIdChanged) {
const autoCompleteListener = (_e, suggestion) => {
onChange?.(suggestion);
if (noteIdChanged) {
const noteId = suggestion?.notePath?.split("/")?.at(-1);
noteIdChanged(noteId);
}
};
const changeListener = (e) => {
if (!ref.current?.value) {
autoCompleteListener(e, null);
}
};
$autoComplete
.on("autocomplete:noteselected", autoCompleteListener)
.on("autocomplete:externallinkselected", autoCompleteListener)
.on("autocomplete:commandselected", autoCompleteListener)
.on("change", changeListener);
return () => {
$autoComplete
.off("autocomplete:noteselected", autoCompleteListener)
.off("autocomplete:externallinkselected", autoCompleteListener)
.off("autocomplete:commandselected", autoCompleteListener)
.off("change", changeListener);
};
if (onTextChange) {
$autoComplete.on("input", inputListener);
}
}, [opts, container?.current, onChange, noteIdChanged])
if (onKeyDown) {
$autoComplete.on("keydown", keyDownListener);
}
if (onBlur) {
$autoComplete.on("blur", blurListener);
}
return () => {
if (onTextChange) {
$autoComplete.off("input", inputListener);
}
if (onKeyDown) {
$autoComplete.off("keydown", keyDownListener);
}
if (onBlur) {
$autoComplete.off("blur", blurListener);
}
};
}, [onBlur, onKeyDown, onTextChange]);
useEffect(() => {
if (!ref.current) return;
const $autoComplete = $(ref.current);
if (!(onChange || noteIdChanged)) {
return;
}
const autoCompleteListener = (_e, suggestion) => {
onChange?.(suggestion);
if (noteIdChanged) {
const noteId = suggestion?.notePath?.split("/")?.at(-1);
noteIdChanged(noteId);
}
};
const changeListener = (e) => {
if (!ref.current?.value) {
autoCompleteListener(e, null);
}
};
$autoComplete
.on("autocomplete:noteselected", autoCompleteListener)
.on("autocomplete:externallinkselected", autoCompleteListener)
.on("autocomplete:commandselected", autoCompleteListener)
.on("change", changeListener);
return () => {
$autoComplete
.off("autocomplete:noteselected", autoCompleteListener)
.off("autocomplete:externallinkselected", autoCompleteListener)
.off("autocomplete:commandselected", autoCompleteListener)
.off("change", changeListener);
};
}, [onChange, noteIdChanged]);
useEffect(() => {
if (!ref.current) return;
const $autoComplete = $(ref.current);
if (noteId) {
$autoComplete.setNote(noteId);
} else if (text) {
note_autocomplete.setText($autoComplete, text);
} else {
$autoComplete.setSelectedNotePath("");
$autoComplete.autocomplete("val", "");
ref.current.value = "";
void $autoComplete.setNote(noteId);
return;
}
if (text !== undefined) {
if (text) {
note_autocomplete.setText($autoComplete, text);
} else {
note_autocomplete.clearText($autoComplete);
}
return;
}
note_autocomplete.clearText($autoComplete);
}, [text, noteId]);
return (