import { createReactBlockSpec } from "@blocknote/react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import type { ChangeEvent } from "react"; import { useSnippetAutocomplete, type SnippetSuggestion } from "../snippetAutocomplete"; type SnippetDropdownProps = { value: string; placeholder: string; suggestions: SnippetSuggestion[]; selectedId: string; onSelect: (suggestion: SnippetSuggestion) => void; }; function SnippetDropdown({ value, placeholder, suggestions, selectedId, onSelect }: SnippetDropdownProps) { const [isOpen, setIsOpen] = useState(false); const [search, setSearch] = useState(""); const containerRef = useRef(null); const searchRef = useRef(null); useEffect(() => { if (!isOpen) return; const handleMouseDown = (event: MouseEvent) => { if (containerRef.current && !containerRef.current.contains(event.target as Node)) { setIsOpen(false); } }; document.addEventListener("mousedown", handleMouseDown); return () => document.removeEventListener("mousedown", handleMouseDown); }, [isOpen]); useEffect(() => { if (isOpen) { setSearch(""); requestAnimationFrame(() => searchRef.current?.focus()); } }, [isOpen]); const filtered = useMemo(() => { const snippets = suggestions.filter((s) => s.isSnippet === true); if (!search) return snippets; const lower = search.toLowerCase(); return snippets.filter((s) => s.title.toLowerCase().includes(lower)); }, [suggestions, search]); const handleSearchChange = useCallback((event: ChangeEvent) => { setSearch(event.target.value); }, []); return (
{isOpen && (
{filtered.map((suggestion) => { const isSelected = suggestion.id === selectedId; return ( ); })} {filtered.length === 0 && (
No snippets found
)}
)}
); } export const snippetBlock = createReactBlockSpec( { type: "snippet", content: "none", propSchema: { snippetId: { default: "", }, snippetTitle: { default: "", }, snippetData: { default: "", }, snippetExpectedResult: { default: "", }, }, }, { render: ({ block, editor }) => { const snippetTitle = (block.props.snippetTitle as string) || ""; const snippetData = (block.props.snippetData as string) || ""; const snippetId = (block.props.snippetId as string) || ""; const snippetSuggestions = useSnippetAutocomplete(); const hasSnippets = snippetSuggestions.length > 0; const isSnippetSelected = snippetId.length > 0; const resolvedTitle = useMemo(() => { if (snippetTitle) return snippetTitle; if (!snippetId || snippetSuggestions.length === 0) return ""; return snippetSuggestions.find((s) => s.id === snippetId)?.title ?? ""; }, [snippetTitle, snippetId, snippetSuggestions]); const handleSnippetSelect = useCallback( (suggestion: SnippetSuggestion) => { const rawBody = suggestion.body ?? ""; const sanitizedBody = rawBody .split(/\r?\n/) .filter((line) => !/^