UNPKG

@firecms/core

Version:

Awesome Firebase/Firestore-based headless open-source CMS

127 lines (108 loc) 5.04 kB
import { useEffect, useRef, useState } from "react"; import { EditorState, Transaction, Plugin } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; import { schema } from "./schema"; import { markdownParser, markdownSerializer } from "./markdown"; import { removeClassesFromJson } from "./utils/remove_classes"; import { DOMParser as ProseMirrorDOMParser, DOMSerializer } from "prosemirror-model"; export interface UseProseMirrorOptions { content?: any; plugins?: Plugin[]; editable?: boolean; onMarkdownContentChange?: (content: string) => void; onJsonContentChange?: (content: any | null) => void; onHtmlContentChange?: (content: string) => void; version?: number; } export function useProseMirror(options: UseProseMirrorOptions) { const mountRef = useRef<HTMLDivElement>(null); const viewRef = useRef<EditorView | null>(null); const [editorState, setEditorState] = useState<EditorState | null>(null); const isUpdatingContentRef = useRef(false); useEffect(() => { if (!mountRef.current) return; let doc; try { if (typeof options.content === "string") { if (options.content.trim() === "") { doc = schema.node("doc", null, [schema.node("paragraph")]); } else if (options.content.startsWith("<")) { const temp = document.createElement("div"); temp.innerHTML = options.content; doc = ProseMirrorDOMParser.fromSchema(schema).parse(temp); } else { doc = markdownParser.parse(options.content); } } else if (options.content && typeof options.content === "object" && Object.keys(options.content).length > 0) { doc = schema.nodeFromJSON(options.content); } else { doc = schema.node("doc", null, [schema.node("paragraph")]); } } catch (e) { console.warn("Failed to parse initial content, falling back to empty doc", e); doc = schema.node("doc", null, [schema.node("paragraph")]); } const state = EditorState.create({ doc, schema, plugins: options.plugins || [], }); const view = new EditorView(mountRef.current, { state, editable: () => options.editable !== false, dispatchTransaction(transaction: Transaction) { const newState = view.state.apply(transaction); view.updateState(newState); setEditorState(newState); if (transaction.docChanged && !isUpdatingContentRef.current) { if (options.onMarkdownContentChange) { options.onMarkdownContentChange(markdownSerializer.serialize(newState.doc)); } if (options.onJsonContentChange) { options.onJsonContentChange(removeClassesFromJson(newState.doc.toJSON())); } if (options.onHtmlContentChange) { const div = document.createElement("div"); const fragment = DOMSerializer.fromSchema(schema).serializeFragment(newState.doc.content); div.appendChild(fragment); options.onHtmlContentChange(div.innerHTML); } } }, }); viewRef.current = view; setEditorState(state); return () => { view.destroy(); viewRef.current = null; }; }, []); // Initialize once // Handle external content updates (like version change) useEffect(() => { if (!viewRef.current || options.version === undefined || options.version <= 0) return; try { isUpdatingContentRef.current = true; let doc; if (typeof options.content === "string") { doc = markdownParser.parse(options.content) || schema.node("doc", null, [schema.node("paragraph")]); } else if (options.content) { doc = schema.nodeFromJSON(options.content); } else { doc = schema.node("doc", null, [schema.node("paragraph")]); } const tr = viewRef.current.state.tr.replaceWith(0, viewRef.current.state.doc.content.size, doc); viewRef.current.dispatch(tr); isUpdatingContentRef.current = false; } catch (e) { console.error("Error updating content manually via version bump:", e); isUpdatingContentRef.current = false; } }, [options.version]); // Handle editable prop change useEffect(() => { if (viewRef.current && editorState) { viewRef.current.setProps({ editable: () => options.editable !== false }); } }, [options.editable, editorState]); return { mountRef, view: viewRef.current, editorState }; }