File
Blob: src/client/components/editor/lib/page-mention/open-picker.ts
| 1 | import type { Editor, Range } from "@tiptap/core"; |
| 2 | import type { PageMentionCandidate } from "@/client/components/page-mention/types"; |
| 3 | import { mountPageMentionPicker, type PageMentionPickerHandle } from "../../controllers/page-mention/picker-overlay"; |
| 4 | import type { PageMentionItem } from "../../controllers/page-mention/picker-panel"; |
| 5 | import "../../extensions/page-mention/commands"; |
| 6 | |
| 7 | const MAX_SUGGESTIONS = 20; |
| 8 | |
| 9 | /** Filter pages by query for the [[ suggestion path. Also caps to MAX_SUGGESTIONS. */ |
| 10 | export 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. */ |
| 23 | function 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 | |
| 31 | interface OpenPickerOpts { |
| 32 | range: Range; |
| 33 | clientRect: (() => DOMRect | null) | null; |
| 34 | candidates: PageMentionCandidate[]; |
| 35 | } |
| 36 | |
| 37 | function 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 | |
| 71 | interface LaunchPickerOpts { |
| 72 | range: Range; |
| 73 | candidates: PageMentionCandidate[]; |
| 74 | } |
| 75 | |
| 76 | export 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 | } |