"use client"; import React, { forwardRef, useEffect, useImperativeHandle, useState } from "react"; import { ReactRenderer } from "@tiptap/react"; import tippy, { type Instance } from "tippy.js"; import { cn } from "@/lib/utils"; export interface Item { id: string; title: string; hint?: string; icon?: React.ReactNode; group?: string; run?: () => void; data?: any } export const SuggestionList = forwardRef<{ onKeyDown: (p: { event: KeyboardEvent }) => boolean }, { items: Item[]; command: (item: Item) => void; empty?: string }>(({ items, command, empty = "No results" }, ref) => { const [idx, setIdx] = useState(0); useEffect(() => setIdx(0), [items]); useImperativeHandle(ref, () => ({ onKeyDown: ({ event }) => { if (event.key === "ArrowUp") { setIdx((i) => (i + items.length - 1) % Math.max(1, items.length)); return true; } if (event.key === "ArrowDown") { setIdx((i) => (i + 1) % Math.max(1, items.length)); return true; } if (event.key === "Enter") { if (items[idx]) command(items[idx]); return true; } return false; } })); useEffect(() => { document.getElementById(`sg-${idx}`)?.scrollIntoView({ block: "nearest" }); }, [idx]); if (!items.length) return
{empty}
; let lastGroup: string | undefined; return (
{items.map((it, i) => { const g = it.group !== lastGroup ? it.group : null; lastGroup = it.group; return ( {g &&
{g}
}
); })}
); }); SuggestionList.displayName = "SuggestionList"; /** Generic Tiptap suggestion renderer (slash commands, mentions) using ReactRenderer + tippy. */ export function makeRenderer(Component: React.ComponentType, extra: Record = {}) { return () => { let component: ReactRenderer | null = null; let popup: Instance[] | null = null; return { onStart: (props: any) => { component = new ReactRenderer(Component, { props: { ...props, ...extra }, editor: props.editor }); if (!props.clientRect) return; popup = tippy("body", { getReferenceClientRect: props.clientRect, appendTo: () => document.body, content: component.element, showOnCreate: true, interactive: true, trigger: "manual", placement: "bottom-start", arrow: false, theme: "none", offset: [0, 6] }); }, onUpdate: (props: any) => { component?.updateProps({ ...props, ...extra }); if (props.clientRect) popup?.[0]?.setProps({ getReferenceClientRect: props.clientRect }); }, onKeyDown: (props: any) => { if (props.event.key === "Escape") { popup?.[0]?.hide(); return true; } return component?.ref?.onKeyDown?.(props) ?? false; }, onExit: () => { popup?.[0]?.destroy(); component?.destroy(); popup = null; component = null; }, }; }; }