carbon-tokenization / frontend /src /editor /FootnoteView.tsx
tfrere's picture
tfrere HF Staff
chore: initial commit
561e6f0
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>
);
}