| import { NodeViewWrapper } from "@tiptap/react"; |
| import { useEffect, useState, useCallback, useRef } from "react"; |
| import type { NodeViewProps } from "@tiptap/react"; |
|
|
| export function BibliographyView({ editor, node, getPos }: NodeViewProps) { |
| const [html, setHtml] = useState(""); |
| const [count, setCount] = useState(0); |
| const [style, setStyle] = useState("apa"); |
| const cacheRef = useRef(""); |
| const timerRef = useRef<ReturnType<typeof setTimeout>>(); |
|
|
| |
| useEffect(() => { |
| const settingsMap = (editor.storage.citation as any)?.settingsMap; |
| if (!settingsMap) return; |
|
|
| const sync = () => { |
| const val = settingsMap.get("citationStyle"); |
| if (val && val !== style) setStyle(val as string); |
| }; |
| sync(); |
| settingsMap.observe(sync); |
| return () => settingsMap.unobserve(sync); |
| }, [editor]); |
|
|
| const refresh = useCallback(() => { |
| const citationsMap = (editor.storage.citation as any)?.citationsMap; |
| if (!citationsMap) return; |
|
|
| const keys = new Set<string>(); |
| editor.state.doc.descendants((node) => { |
| if (node.type.name === "citation" && node.attrs.key) { |
| keys.add(node.attrs.key as string); |
| } |
| }); |
|
|
| const entries: any[] = []; |
| for (const key of keys) { |
| const entry = citationsMap.get(key); |
| if (entry) entries.push({ ...entry, id: key }); |
| } |
|
|
| setCount(entries.length); |
|
|
| if (!entries.length) { |
| setHtml(""); |
| cacheRef.current = ""; |
| return; |
| } |
|
|
| const cacheKey = entries |
| .map((e) => e.id) |
| .sort() |
| .join(",") + `:${style}`; |
| if (cacheKey === cacheRef.current) return; |
| cacheRef.current = cacheKey; |
|
|
| fetch("/api/citations/format", { |
| method: "POST", |
| headers: { "Content-Type": "application/json" }, |
| body: JSON.stringify({ entries, style }), |
| }) |
| .then((r) => r.json()) |
| .then((data) => { |
| if (data.html) { |
| setHtml(data.html); |
| |
| const pos = getPos(); |
| if (typeof pos === "number" && node.attrs.renderedHtml !== data.html) { |
| editor.commands.command(({ tr }) => { |
| tr.setNodeMarkup(pos, undefined, { ...node.attrs, renderedHtml: data.html }); |
| return true; |
| }); |
| } |
| } |
| }) |
| .catch(console.error); |
| }, [editor, style]); |
|
|
| useEffect(() => { |
| const debouncedRefresh = () => { |
| clearTimeout(timerRef.current); |
| timerRef.current = setTimeout(refresh, 500); |
| }; |
|
|
| refresh(); |
|
|
| const citationsMap = (editor.storage.citation as any)?.citationsMap; |
| if (citationsMap) citationsMap.observe(debouncedRefresh); |
|
|
| const onUpdate = () => debouncedRefresh(); |
| editor.on("update", onUpdate); |
|
|
| return () => { |
| clearTimeout(timerRef.current); |
| if (citationsMap) citationsMap.unobserve(debouncedRefresh); |
| editor.off("update", onUpdate); |
| }; |
| }, [editor, refresh]); |
|
|
| return ( |
| <NodeViewWrapper> |
| <div className="bibliography-block" contentEditable={false}> |
| <h2 className="bibliography-title">References</h2> |
| {count === 0 ? ( |
| <p className="bibliography-empty"> |
| No citations yet. Use /Citation to add references. |
| </p> |
| ) : ( |
| <div |
| className="bibliography-content" |
| dangerouslySetInnerHTML={{ __html: html }} |
| /> |
| )} |
| </div> |
| </NodeViewWrapper> |
| ); |
| } |
|
|