106 lines
3.3 KiB
TypeScript
106 lines
3.3 KiB
TypeScript
"use client";
|
|
|
|
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
import { useEntityReferenceSearch } from "@/hooks/use-entity-reference-search";
|
|
import type { EntityReference } from "@/lib/types/entity-reference";
|
|
import {
|
|
createEntityReferenceInputGate,
|
|
createEntityReferenceSuggestion,
|
|
handleEntityReferenceMenuKeyDown,
|
|
} from "./tiptap-entity-reference-suggestion";
|
|
import type { MenuState } from "./tiptap-suggestion";
|
|
import { selectableEntityReferences } from "./entity-reference-groups";
|
|
|
|
const EMPTY_REFERENCE_MENU: MenuState<EntityReference> = {
|
|
isOpen: false,
|
|
items: [],
|
|
query: "",
|
|
clientRect: null,
|
|
command: null,
|
|
};
|
|
|
|
type UseEntityReferenceComposerOptions = {
|
|
enabled: boolean;
|
|
workspaceId: string | null;
|
|
sessionId: string | null;
|
|
};
|
|
|
|
export function useEntityReferenceComposer({
|
|
enabled,
|
|
workspaceId,
|
|
sessionId,
|
|
}: UseEntityReferenceComposerOptions) {
|
|
const inputGate = useMemo(() => createEntityReferenceInputGate(), []);
|
|
const [menu, setMenu] = useState<MenuState<EntityReference>>(EMPTY_REFERENCE_MENU);
|
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
const searchEnabled =
|
|
enabled && Boolean(workspaceId) && menu.isOpen && Boolean(menu.query.trim());
|
|
const search = useEntityReferenceSearch({
|
|
workspaceId,
|
|
sessionId,
|
|
query: menu.query,
|
|
enabled: searchEnabled,
|
|
});
|
|
const items = useMemo(() => selectableEntityReferences(search.groups), [search.groups]);
|
|
const clampedSelectedIndex = Math.min(selectedIndex, Math.max(0, items.length - 1));
|
|
const itemsRef = useRef(items);
|
|
const selectedIndexRef = useRef(clampedSelectedIndex);
|
|
const commandRef = useRef(menu.command);
|
|
|
|
useLayoutEffect(() => {
|
|
itemsRef.current = items;
|
|
selectedIndexRef.current = clampedSelectedIndex;
|
|
commandRef.current = menu.command;
|
|
});
|
|
|
|
useEffect(() => {
|
|
void Promise.resolve().then(() => setSelectedIndex(0));
|
|
}, [items]);
|
|
|
|
useEffect(() => {
|
|
inputGate.reset();
|
|
void Promise.resolve().then(() => setMenu(EMPTY_REFERENCE_MENU));
|
|
}, [enabled, inputGate, sessionId, workspaceId]);
|
|
|
|
const selectReference = useCallback((reference: EntityReference) => {
|
|
commandRef.current?.(reference);
|
|
setMenu(EMPTY_REFERENCE_MENU);
|
|
}, []);
|
|
const onKeyDown = useCallback(
|
|
(event: KeyboardEvent) =>
|
|
handleEntityReferenceMenuKeyDown({
|
|
event,
|
|
items: itemsRef.current,
|
|
selectedIndex: selectedIndexRef.current,
|
|
setSelectedIndex,
|
|
onSelect: selectReference,
|
|
}),
|
|
[selectReference],
|
|
);
|
|
const suggestion = useMemo(
|
|
() => createEntityReferenceSuggestion(setMenu, onKeyDown, inputGate),
|
|
[inputGate, onKeyDown],
|
|
);
|
|
const close = useCallback(() => setMenu(EMPTY_REFERENCE_MENU), []);
|
|
const hasWorkspace = Boolean(workspaceId);
|
|
|
|
return {
|
|
suggestion: enabled ? suggestion : undefined,
|
|
isOpen: enabled && hasWorkspace && menu.isOpen,
|
|
clientRect: menu.clientRect,
|
|
query: menu.query,
|
|
groups: search.groups,
|
|
isSearching: search.isSearching,
|
|
error: search.error,
|
|
retry: search.retry,
|
|
selectedIndex: clampedSelectedIndex,
|
|
setSelectedIndex,
|
|
selectReference,
|
|
close,
|
|
onTextInput: inputGate.recordTextInput,
|
|
onBeforeInput: (inputType: string) => {
|
|
if (inputType.startsWith("delete")) inputGate.recordDeletion();
|
|
},
|
|
};
|
|
}
|