"use client"; import { Bold, Italic, Underline, Strikethrough, List, ListOrdered, Link2, X, } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import { createEditor, Editor, Element, Transforms, type Descendant, } from "slate"; import { withHistory } from "slate-history"; import { Editable, Slate, withReact, ReactEditor, type RenderElementProps, type RenderLeafProps, } from "slate-react"; import { type NoteDocument, type NoteParagraph, safeNoteLink, serializeNoteDocument, } from "@/lib/flow-board/topic-notes"; export function XmindNotesEditor({ value, onChange, }: { value: NoteDocument; onChange: (value: NoteDocument) => void; }) { const [editor] = useState(() => withHistory(withReact(createEditor()))); const sourceRef = useRef(value); sourceRef.current = value; const onChangeRef = useRef(onChange); onChangeRef.current = onChange; const [revision, setRevision] = useState(0); const [linkOpen, setLinkOpen] = useState(false), [link, setLink] = useState(""); const [linkError, setLinkError] = useState(""); const savedSelection = useRef(editor.selection); useEffect(() => { if ( JSON.stringify(serializeNoteDocument(editor.children as NoteDocument)) === JSON.stringify(serializeNoteDocument(value)) ) return; editor.children = value as Descendant[]; editor.selection = null; editor.history = { undos: [], redos: [] }; editor.onChange(); }, [editor, value]); const marks = Editor.marks(editor) as Record | null; // Toolbar presses can arrive before Slate's throttled selection listener. const syncDOMSelection = () => { const selection = window.getSelection(); if ( !selection?.anchorNode || !ReactEditor.hasDOMNode(editor, selection.anchorNode) ) return; const range = ReactEditor.toSlateRange(editor, selection, { exactMatch: false, suppressThrow: true, }); if (range) Transforms.select(editor, range); }; const toggleMark = ( mark: "bold" | "italic" | "underline" | "strikethrough" ) => { syncDOMSelection(); const currentMarks = Editor.marks(editor) as Record | null; if (currentMarks?.[mark]) Editor.removeMark(editor, mark); else Editor.addMark(editor, mark, true); setRevision((v) => v + 1); }; const toggleList = (list: "bullet" | "number") => { syncDOMSelection(); const match = Array.from( Editor.nodes(editor, { match: (n) => Element.isElement(n) }) )[0]; Transforms.setNodes( editor, { list: (match?.[0] as NoteParagraph)?.list === list ? undefined : list, } as Partial, { match: (n) => Element.isElement(n) } ); }; const applyLink = () => { const href = link.trim() ? safeNoteLink(link.trim()) : ""; if (href === null) { setLinkError("请输入完整的网页或邮件链接。"); return; } if (savedSelection.current) Transforms.select(editor, savedSelection.current); if (href) Editor.addMark(editor, "href", href); else Editor.removeMark(editor, "href"); setLinkOpen(false); editor.onChange(); }; const leaf = ({ attributes, children, leaf }: RenderLeafProps) => { const t = leaf as { bold?: boolean; italic?: boolean; underline?: boolean; strikethrough?: boolean; href?: string; }; const href = safeNoteLink(t.href || ""); return ( {href ? ( { if (!e.metaKey && !e.ctrlKey) e.preventDefault(); }} target="_blank" > {children} ) : ( children )} ); }; const element = ({ attributes, children, element }: RenderElementProps) => { const p = element as NoteParagraph; return (

{children}

); }; return (
{ setRevision((v) => v + 1); if (editor.operations.every((o) => o.type === "set_selection")) return; const doc = next as NoteDocument; if ( JSON.stringify(serializeNoteDocument(doc)) !== JSON.stringify(serializeNoteDocument(sourceRef.current)) ) onChangeRef.current(doc); }} >
{( [ ["bold", "加粗", Bold], ["italic", "斜体", Italic], ["underline", "下划线", Underline], ["strikethrough", "删除线", Strikethrough], ] as const ).map(([mark, label, Icon]) => ( ))}
{linkOpen ? (
setLink(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); applyLink(); } }} /> {linkError ?

{linkError}

: null}
) : null} { if ( (e.metaKey || e.ctrlKey) && ["b", "i", "u"].includes(e.key.toLowerCase()) ) { e.preventDefault(); toggleMark( ( { b: "bold", i: "italic", u: "underline" } as Record< string, "bold" | "italic" | "underline" > )[e.key.toLowerCase()] ); } if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "z") e.stopPropagation(); if (e.key === "Tab") { e.preventDefault(); const match = Array.from( Editor.nodes(editor, { match: (n) => Element.isElement(n) }) )[0]; const indent = (match?.[0] as NoteParagraph)?.indent || 0; Transforms.setNodes( editor, { indent: Math.max( 0, Math.min(120, indent + (e.shiftKey ? -20 : 20)) ), } as Partial, { match: (n) => Element.isElement(n) } ); } }} />
); }