Skip to content
File

Blob: src/client/components/editor/lib/page-mention/open-picker.ts

typescript89 lines
1import type { Editor, Range } from "@tiptap/core";
2import type { PageMentionCandidate } from "@/client/components/page-mention/types";
3import { mountPageMentionPicker, type PageMentionPickerHandle } from "../../controllers/page-mention/picker-overlay";
4import type { PageMentionItem } from "../../controllers/page-mention/picker-panel";
5import "../../extensions/page-mention/commands";
6 
7const MAX_SUGGESTIONS = 20;
8 
9/** Filter pages by query for the [[ suggestion path. Also caps to MAX_SUGGESTIONS. */
10export function filterPageMentionItems(candidates: PageMentionCandidate[], query: string): PageMentionItem[] {
11 const q = query.trim().toLowerCase();
12 const filtered: PageMentionItem[] = [];
13 for (const candidate of candidates) {
14 const title = candidate.title || "Untitled";
15 if (q.length > 0 && !title.toLowerCase().includes(q)) continue;
16 filtered.push({ pageId: candidate.pageId, title, icon: candidate.icon });
17 if (filtered.length >= MAX_SUGGESTIONS) break;
18 }
19 return filtered;
20}
21 
22/** Build the full unfiltered item list for the slash-launched picker. */
23function collectAllVisibleItems(candidates: PageMentionCandidate[]): PageMentionItem[] {
24 return candidates.map((candidate) => ({
25 pageId: candidate.pageId,
26 title: candidate.title || "Untitled",
27 icon: candidate.icon,
28 }));
29}
30 
31interface OpenPickerOpts {
32 range: Range;
33 clientRect: (() => DOMRect | null) | null;
34 candidates: PageMentionCandidate[];
35}
36 
37function openPageMentionPicker(editor: Editor, opts: OpenPickerOpts): PageMentionPickerHandle {
38 const items = collectAllVisibleItems(opts.candidates);
39 
40 let handle: PageMentionPickerHandle | null = null;
41 
42 function cleanup() {
43 editor.off("destroy", cleanup);
44 handle?.destroy();
45 handle = null;
46 }
47 
48 const command = (item: PageMentionItem) => {
49 editor.commands.insertPageMention({ pageId: item.pageId, range: opts.range });
50 cleanup();
51 };
52 
53 handle = mountPageMentionPicker(editor, {
54 items,
55 filterMode: "internal",
56 command,
57 clientRect: opts.clientRect,
58 contextElement: editor.view.dom,
59 onCancel: cleanup,
60 });
61 
62 editor.on("destroy", cleanup);
63 
64 return {
65 updateProps: (next) => handle?.updateProps(next),
66 onKeyDown: (e) => handle?.onKeyDown(e) ?? false,
67 destroy: cleanup,
68 };
69}
70 
71interface LaunchPickerOpts {
72 range: Range;
73 candidates: PageMentionCandidate[];
74}
75 
76export function launchPageMentionPicker(editor: Editor, opts: LaunchPickerOpts): PageMentionPickerHandle {
77 editor.chain().focus(null, { scrollIntoView: false }).deleteRange(opts.range).run();
78 const from = editor.state.selection.from;
79 
80 return openPageMentionPicker(editor, {
81 range: { from, to: from },
82 candidates: opts.candidates,
83 clientRect: () => {
84 const coords = editor.view.coordsAtPos(from);
85 return new DOMRect(coords.left, coords.top, 0, coords.bottom - coords.top);
86 },
87 });
88}