/** * Callout headings. * * Type and title are node attributes rather than content, so they render as * chrome and are edited through controls rather than by typing into the block. * Changing either is a local Yjs update — callouts already carry an id, so * nothing here needs the server. */ import { useCallback, useEffect, useState } from "react"; import { createPortal } from "react-dom"; import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; import { readOnly$ } from "@mdxeditor/editor"; import { useCellValue } from "@mdxeditor/gurx"; import { $getNodeByKey, $getRoot, $isElementNode } from "lexical"; import { $isCalloutNode, CALLOUT_TYPES } from "@chopin/dialect"; import type { CalloutType } from "@chopin/dialect"; import type { ElementNode, LexicalEditor } from "lexical"; type Callout = { key: string; type: CalloutType; title: string }; const LABELS: Record = { note: "Note", tip: "Tip", important: "Important", warning: "Warning", danger: "Danger", }; function collect(editor: LexicalEditor): Callout[] { let out: Callout[] = []; editor.getEditorState().read(() => { let walk = (node: ElementNode) => { for (let child of node.getChildren()) { if ($isCalloutNode(child)) { out.push({ key: child.getKey(), type: child.getCalloutType(), title: child.getTitle(), }); } if ($isElementNode(child)) walk(child); } }; walk($getRoot()); }); return out; } function Heading( { callout, editor, disabled }: { callout: Callout; editor: LexicalEditor; disabled?: boolean; }, ) { let setType = useCallback((type: CalloutType) => { editor.update(() => { let node = $getNodeByKey(callout.key); if ($isCalloutNode(node)) node.setCalloutType(type); }); }, [editor, callout.key]); let setTitle = useCallback((title: string) => { editor.update(() => { let node = $getNodeByKey(callout.key); if ($isCalloutNode(node)) node.setTitle(title); }); }, [editor, callout.key]); return (
setTitle(event.currentTarget.value)} className="min-w-0 flex-1 rounded-sm control-edge bg-page px-1 text-sm font-medium text-text-primary placeholder:text-text-tertiary focus-visible:border-brand" />
); } export function CalloutPlugin() { let [editor] = useLexicalComposerContext(); let disabled = useCellValue(readOnly$); let [callouts, setCallouts] = useState([]); useEffect(() => { let update = () => setCallouts(collect(editor)); update(); return editor.registerUpdateListener(update); }, [editor]); return ( <> {callouts.map(callout => { let host = editor.getElementByKey(callout.key) ?.querySelector("[data-plan-chrome='callout']"); if (!host) return null; return createPortal( , host, callout.key, ); })} ); }