// Suggestion plugin config that wires the slash menu into TipTap. // Renders SlashMenu in a fixed-position floating panel near the caret. "use client"; import type { Editor, Range } from "@tiptap/core"; import type { SuggestionOptions, SuggestionProps, SuggestionKeyDownProps } from "@tiptap/suggestion"; import { createRoot, type Root } from "react-dom/client"; import { createElement, createRef } from "react"; import { SlashMenu, SLASH_ITEMS, type SlashItem, type SlashMenuHandle } from "./SlashMenu"; export const slashSuggestion: Omit, "editor"> = { char: "/", startOfLine: false, allowSpaces: false, items: ({ query }) => SLASH_ITEMS.filter( item => query.length === 0 || item.kind.toLowerCase().startsWith(query.toLowerCase()) || item.label.toLowerCase().startsWith(query.toLowerCase()) ), command: ({ editor, range, props }: { editor: Editor; range: Range; props: SlashItem }) => { editor .chain() .focus() .deleteRange(range) .insertChip({ kind: props.kind, refId: null, label: props.kind === "requirement" ? "REQ-001" : "untitled", }) .run(); }, render: () => { let container: HTMLDivElement | null = null; let root: Root | null = null; const handleRef = createRef(); function position(rect: DOMRect | null) { if (!container || !rect) return; container.style.position = "fixed"; container.style.top = `${rect.bottom + 6}px`; container.style.left = `${rect.left}px`; container.style.zIndex = "1000"; } function rerender(props: SuggestionProps) { if (!root) return; root.render( createElement(SlashMenu, { ref: handleRef, query: props.query, command: (item: SlashItem) => props.command(item), }) ); } return { onStart(props) { container = document.createElement("div"); document.body.appendChild(container); root = createRoot(container); rerender(props); position(props.clientRect?.() ?? null); }, onUpdate(props) { rerender(props); position(props.clientRect?.() ?? null); }, onKeyDown(props: SuggestionKeyDownProps) { if (props.event.key === "Escape") { props.event.preventDefault(); return true; } return handleRef.current?.onKeyDown(props.event) ?? false; }, onExit() { if (root) root.unmount(); if (container && container.parentNode) container.parentNode.removeChild(container); root = null; container = null; }, }; }, };