SurfSense/surfsense_web/components/BlockNoteEditor.tsx

167 lines
4.5 KiB
TypeScript
Raw Normal View History

2025-11-23 15:23:31 +05:30
"use client";
import { useTheme } from "next-themes";
2025-11-23 16:39:23 +05:30
import { useEffect, useMemo, useRef } from "react";
2025-11-23 15:23:31 +05:30
import "@blocknote/core/fonts/inter.css";
import "@blocknote/mantine/style.css";
import { BlockNoteView } from "@blocknote/mantine";
2025-11-23 16:39:23 +05:30
import { useCreateBlockNote } from "@blocknote/react";
2025-11-23 15:23:31 +05:30
interface BlockNoteEditorProps {
2025-11-23 16:39:23 +05:30
initialContent?: any;
onChange?: (content: any) => void;
useTitleBlock?: boolean; // Whether to use first block as title (Notion-style)
2025-11-23 15:23:31 +05:30
}
// Helper to ensure first block is a heading for title
function ensureTitleBlock(content: any[] | undefined): any[] {
if (!content || content.length === 0) {
// Return empty heading block for new notes
return [
{
type: "heading",
props: { level: 1 },
content: [],
children: [],
},
];
}
// If first block is not a heading, convert it to one
const firstBlock = content[0];
if (firstBlock?.type !== "heading") {
// Extract text from first block
let titleText = "";
if (firstBlock?.content && Array.isArray(firstBlock.content)) {
titleText = firstBlock.content
.map((item: any) => {
if (typeof item === "string") return item;
if (item?.text) return item.text;
return "";
})
.join("")
.trim();
}
// Create heading block with extracted text
const titleBlock = {
type: "heading",
props: { level: 1 },
content: titleText
? [
{
type: "text",
text: titleText,
styles: {},
},
]
: [],
children: [],
};
// Replace first block with heading, keep rest
return [titleBlock, ...content.slice(1)];
}
return content;
}
export default function BlockNoteEditor({
initialContent,
onChange,
useTitleBlock = false,
}: BlockNoteEditorProps) {
2025-11-23 16:39:23 +05:30
const { resolvedTheme } = useTheme();
// Track the initial content to prevent re-initialization
const initialContentRef = useRef<any>(null);
const isInitializedRef = useRef(false);
// Prepare initial content - ensure first block is a heading if useTitleBlock is true
const preparedInitialContent = useMemo(() => {
if (initialContentRef.current !== null) {
return undefined; // Already initialized
}
if (initialContent === undefined) {
// New note - create empty heading block
return useTitleBlock
? [
{
type: "heading",
props: { level: 1 },
content: [],
children: [],
},
]
: undefined;
}
// Existing note - ensure first block is heading
return useTitleBlock ? ensureTitleBlock(initialContent) : initialContent;
}, [initialContent, useTitleBlock]);
2025-11-23 16:39:23 +05:30
// Creates a new editor instance - only use initialContent on first render
const editor = useCreateBlockNote({
initialContent: initialContentRef.current === null ? preparedInitialContent : undefined,
2025-11-23 16:39:23 +05:30
});
// Store initial content on first render only
useEffect(() => {
if (preparedInitialContent !== undefined && initialContentRef.current === null) {
initialContentRef.current = preparedInitialContent;
isInitializedRef.current = true;
} else if (preparedInitialContent === undefined && initialContentRef.current === null) {
// Mark as initialized even when initialContent is undefined (for new notes)
2025-11-23 16:39:23 +05:30
isInitializedRef.current = true;
}
}, [preparedInitialContent]);
2025-11-23 16:39:23 +05:30
// Call onChange when document changes (but don't update from props)
useEffect(() => {
if (!onChange || !editor) return;
// For new notes (no initialContent), we need to wait for editor to be ready
// Use a small delay to ensure editor is fully initialized
if (!isInitializedRef.current) {
const timer = setTimeout(() => {
isInitializedRef.current = true;
}, 100);
return () => clearTimeout(timer);
}
2025-11-23 16:39:23 +05:30
const handleChange = () => {
onChange(editor.document);
};
// Subscribe to document changes
const unsubscribe = editor.onChange(handleChange);
2025-11-23 15:23:31 +05:30
// Also call onChange once with current document to capture initial state
// This ensures we capture content even if user doesn't make changes
if (editor.document) {
onChange(editor.document);
}
2025-11-23 16:39:23 +05:30
return () => {
unsubscribe();
};
}, [editor, onChange]);
2025-11-23 15:23:31 +05:30
2025-11-23 16:39:23 +05:30
// Determine theme for BlockNote with custom dark mode background
const blockNoteTheme = useMemo(() => {
if (resolvedTheme === "dark") {
// Custom dark theme - only override editor background, let BlockNote handle the rest
return {
colors: {
editor: {
background: "#0A0A0A", // Custom dark background
},
},
};
}
return "light" as const;
}, [resolvedTheme]);
2025-11-23 16:39:23 +05:30
// Renders the editor instance
return <BlockNoteView editor={editor} theme={blockNoteTheme} />;
2025-11-23 15:23:31 +05:30
}