| 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); |
|
|
| |
| 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> |
| ); |
| } |
|
|