import { useEffect, useCallback, useMemo, useRef } from 'react' import { trackEvent } from '../lib/telemetry' import { useCreateBlockNote, SuggestionMenuController } from '@blocknote/react' import { BlockNoteView } from '@blocknote/mantine' import { useEditorTheme } from '../hooks/useTheme' import { useImageDrop } from '../hooks/useImageDrop' import { buildTypeEntryMap } from '../utils/typeColors' import { preFilterWikilinks, deduplicateByPath, MIN_QUERY_LENGTH } from '../utils/wikilinkSuggestions' import { filterPersonMentions, PERSON_MENTION_MIN_QUERY } from '../utils/personMentionSuggestions' import { attachClickHandlers, enrichSuggestionItems } from '../utils/suggestionEnrichment' import { WikilinkSuggestionMenu, type WikilinkSuggestionItem } from './WikilinkSuggestionMenu' import type { VaultEntry } from '../types' import { _wikilinkEntriesRef } from './editorSchema' import { useEditorLinkActivation } from './useEditorLinkActivation' const TEST_TABLE_MARKDOWN = `| Head 1 | Head 2 | Head 3 | | --- | --- | --- | | A | B | C | | D | E | F | ` type TestTableBlock = { type?: string content?: { type?: string; columnWidths?: Array } } function applySeededColumnWidths( parsedBlocks: Array, columnWidths?: Array, ) { const tableBlock = parsedBlocks[0] const tableContent = tableBlock?.content if ( !columnWidths || tableBlock?.type !== 'table' || tableContent?.type !== 'tableContent' ) { return } tableContent.columnWidths = [...columnWidths] } async function seedEditorWithTestTable( editor: ReturnType, columnWidths?: Array, ) { const parsedBlocks = await Promise.resolve( editor.tryParseMarkdownToBlocks(TEST_TABLE_MARKDOWN), ) as Array applySeededColumnWidths(parsedBlocks, columnWidths) const tableHtml = editor.blocksToHTMLLossy([ ...parsedBlocks, { type: 'paragraph', content: [], children: [] }, ] as typeof editor.document) editor._tiptapEditor.commands.setContent(tableHtml) editor.focus() } function useSeedBlockNoteTableBridge(editor: ReturnType) { useEffect(() => { const seedBlockNoteTable = (columnWidths?: Array) => ( seedEditorWithTestTable(editor, columnWidths) ) window.__laputaTest = { ...window.__laputaTest, seedBlockNoteTable, } return () => { if (window.__laputaTest?.seedBlockNoteTable === seedBlockNoteTable) { delete window.__laputaTest.seedBlockNoteTable } } }, [editor]) } /** Insert an image block after the current cursor position. */ function useInsertImageCallback(editor: ReturnType) { const editorRef = useRef(editor) useEffect(() => { editorRef.current = editor }, [editor]) return useCallback((url: string) => { const e = editorRef.current const cursorBlock = e.getTextCursorPosition().block e.insertBlocks([{ type: 'image' as const, props: { url } }], cursorBlock, 'after') }, []) } /** Single BlockNote editor view — content is swapped via replaceBlocks */ export function SingleEditorView({ editor, entries, onNavigateWikilink, onChange, vaultPath, editable = true }: { editor: ReturnType entries: VaultEntry[] onNavigateWikilink: (target: string) => void onChange?: () => void vaultPath?: string editable?: boolean }) { const { cssVars } = useEditorTheme() const containerRef = useRef(null) const onImageUrl = useInsertImageCallback(editor) const { isDragOver } = useImageDrop({ containerRef, onImageUrl, vaultPath }) useEditorLinkActivation(containerRef, onNavigateWikilink) const handleContainerClick = useCallback((e: React.MouseEvent) => { if (!editable) return const target = e.target as HTMLElement if (target.closest('[contenteditable="true"]')) return const blocks = editor.document if (blocks.length > 0) { editor.setTextCursorPosition(blocks[blocks.length - 1].id, 'end') } editor.focus() }, [editor, editable]) useEffect(() => { _wikilinkEntriesRef.current = entries }, [entries]) useSeedBlockNoteTableBridge(editor) const typeEntryMap = useMemo(() => buildTypeEntryMap(entries), [entries]) const baseItems = useMemo( () => deduplicateByPath(entries.map(entry => ({ title: entry.title, aliases: [...new Set([entry.filename.replace(/\.md$/, ''), ...entry.aliases])], group: entry.isA || 'Note', entryTitle: entry.title, path: entry.path, }))), [entries] ) const insertWikilink = useCallback((target: string) => { editor.insertInlineContent([ { type: 'wikilink' as const, props: { target } }, " ", ]) trackEvent('wikilink_inserted') }, [editor]) const getWikilinkItems = useCallback(async (query: string): Promise => { if (query.length < MIN_QUERY_LENGTH) return [] const candidates = preFilterWikilinks(baseItems, query) const items = attachClickHandlers(candidates, insertWikilink, vaultPath ?? '') return enrichSuggestionItems(items, query, typeEntryMap) }, [baseItems, insertWikilink, typeEntryMap, vaultPath]) const getPersonMentionItems = useCallback(async (query: string): Promise => { if (query.length < PERSON_MENTION_MIN_QUERY) return [] const candidates = filterPersonMentions(baseItems, query) const items = attachClickHandlers(candidates, insertWikilink, vaultPath ?? '') return enrichSuggestionItems(items, query, typeEntryMap) }, [baseItems, insertWikilink, typeEntryMap, vaultPath]) return (
{isDragOver && (
Drop image here
)} item.onItemClick()} /> item.onItemClick()} />
) }