File size: 3,609 Bytes
561e6f0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
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>
  );
}