Spaces:
Runtime error
Runtime error
| import { NodeViewWrapper } from "@tiptap/react"; | |
| import { useState, useEffect, useRef, useCallback } from "react"; | |
| import type { NodeViewProps } from "@tiptap/react"; | |
| export function FootnoteView({ node, editor, getPos, updateAttributes }: NodeViewProps) { | |
| const content = node.attrs.content as string; | |
| const [index, setIndex] = useState(0); | |
| const [showTooltip, setShowTooltip] = useState(false); | |
| const [editing, setEditing] = useState(false); | |
| const [draft, setDraft] = useState(content); | |
| const tooltipTimer = useRef<ReturnType<typeof setTimeout>>(); | |
| const inputRef = useRef<HTMLTextAreaElement>(null); | |
| // Auto-number: count all footnotes in document order | |
| useEffect(() => { | |
| const computeIndex = () => { | |
| let i = 0; | |
| const pos = getPos(); | |
| editor.state.doc.descendants((n, p) => { | |
| if (n.type.name === "footnote") { | |
| i++; | |
| if (p === pos) setIndex(i); | |
| } | |
| }); | |
| }; | |
| computeIndex(); | |
| editor.on("update", computeIndex); | |
| return () => { editor.off("update", computeIndex); }; | |
| }, [editor, getPos]); | |
| useEffect(() => { | |
| if (editing && inputRef.current) { | |
| inputRef.current.focus(); | |
| inputRef.current.select(); | |
| } | |
| }, [editing]); | |
| const handleMouseEnter = useCallback(() => { | |
| if (!editing) tooltipTimer.current = setTimeout(() => setShowTooltip(true), 300); | |
| }, [editing]); | |
| const handleMouseLeave = useCallback(() => { | |
| clearTimeout(tooltipTimer.current); | |
| if (!editing) setShowTooltip(false); | |
| }, [editing]); | |
| const startEdit = useCallback(() => { | |
| setDraft(content); | |
| setEditing(true); | |
| setShowTooltip(true); | |
| }, [content]); | |
| const commitEdit = useCallback(() => { | |
| updateAttributes({ content: draft }); | |
| setEditing(false); | |
| setShowTooltip(false); | |
| }, [draft, updateAttributes]); | |
| const remove = useCallback(() => { | |
| const pos = getPos(); | |
| if (typeof pos === "number") { | |
| editor.chain().focus().deleteRange({ from: pos, to: pos + 1 }).run(); | |
| } | |
| }, [editor, getPos]); | |
| return ( | |
| <NodeViewWrapper | |
| as="span" | |
| className="footnote-node" | |
| onMouseEnter={handleMouseEnter} | |
| onMouseLeave={handleMouseLeave} | |
| onDoubleClick={startEdit} | |
| > | |
| <sup className="footnote-marker">{index}</sup> | |
| {showTooltip && ( | |
| <div className="footnote-tooltip"> | |
| {editing ? ( | |
| <textarea | |
| ref={inputRef} | |
| className="footnote-tooltip-input" | |
| value={draft} | |
| onChange={(e) => setDraft(e.target.value)} | |
| onKeyDown={(e) => { | |
| if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); commitEdit(); } | |
| if (e.key === "Escape") { setEditing(false); setShowTooltip(false); } | |
| }} | |
| rows={3} | |
| /> | |
| ) : ( | |
| <div className="footnote-tooltip-content">{content || "Empty footnote. Double-click to edit."}</div> | |
| )} | |
| <div className="footnote-tooltip-actions"> | |
| {editing ? ( | |
| <button className="footnote-tooltip-btn" onMouseDown={(e) => { e.preventDefault(); commitEdit(); }}>Save</button> | |
| ) : ( | |
| <> | |
| <button className="footnote-tooltip-btn" onMouseDown={(e) => { e.preventDefault(); startEdit(); }}>Edit</button> | |
| <button className="footnote-tooltip-btn footnote-tooltip-remove" onMouseDown={(e) => { e.preventDefault(); e.stopPropagation(); remove(); }}>Remove</button> | |
| </> | |
| )} | |
| </div> | |
| </div> | |
| )} | |
| </NodeViewWrapper> | |
| ); | |
| } | |