UNPKG

@firecms/core

Version:

Awesome Firebase/Firestore-based headless open-source CMS

125 lines (111 loc) 4.65 kB
import { useState, useEffect, useRef, useLayoutEffect } from "react"; import { EditorState, Transaction, Plugin } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; import { schema } from "../schema"; import { corePlugins } from "../plugins"; import { parser } from "../markdown"; import { nodeViews } from "../nodeViews"; import { createDropImagePlugin } from "../extensions/Image"; import { columnResizing, tableEditing } from "prosemirror-tables"; const trailingNodePlugin = new Plugin({ appendTransaction: (_, oldState, newState) => { const doc = newState.doc; if (doc.lastChild && doc.lastChild.type.name !== "paragraph") { return newState.tr.insert(doc.content.size, newState.schema.nodes.paragraph.create()); } return null; } }); interface UseProseMirrorProps { initialContent?: string | any; editable?: boolean; handleImageUpload?: (file: File) => Promise<string>; } export function useProseMirror({ initialContent, editable = true, handleImageUpload }: UseProseMirrorProps) { const plugins = [ ...corePlugins, columnResizing(), tableEditing(), trailingNodePlugin ]; if (handleImageUpload) { plugins.push(createDropImagePlugin(handleImageUpload)); } const defaultState = EditorState.create({ doc: typeof initialContent === "string" ? parser.parse(initialContent) : initialContent ? schema.nodeFromJSON(initialContent) : schema.node("doc", null, [schema.node("paragraph")]), schema, plugins }); const [state, setState] = useState<EditorState>(defaultState); const [view, setView] = useState<EditorView | null>(null); const editorRef = useRef<HTMLDivElement>(null); const viewRef = useRef<EditorView | null>(null); useLayoutEffect(() => { if (!editorRef.current) return; const editorView = new EditorView(editorRef.current, { state: defaultState, editable: () => editable, dispatchTransaction: (tr: Transaction) => { const newState = editorView.state.apply(tr); editorView.updateState(newState); setState(newState); }, nodeViews: nodeViews, transformPastedHTML(html: string) { // Strip inline styles and classes from pasted HTML so we don't // get textStyle marks (color, font-size, etc.) that have no // markdown representation. This makes paste look consistent. const div = document.createElement("div"); div.innerHTML = html; div.querySelectorAll("*").forEach((el) => { el.removeAttribute("style"); el.removeAttribute("class"); el.removeAttribute("color"); el.removeAttribute("bgcolor"); el.removeAttribute("face"); }); return div.innerHTML; }, }); // Patch posAtCoords to allow dropping/interacting anywhere horizontally natively const originalPosAtCoords = editorView.posAtCoords.bind(editorView); editorView.posAtCoords = (coords: { left: number, top: number }) => { let res = originalPosAtCoords(coords); if (!res) { const editorRect = editorView.dom.getBoundingClientRect(); // If it's literally anywhere to the left of the actual ProseMirror content block if (coords.left <= editorRect.left) { const probeX = editorRect.left + Math.min(60, editorRect.width / 4); return originalPosAtCoords({ left: probeX, top: coords.top }); } // Or if it's anywhere to the right if (coords.left >= editorRect.right) { const probeX = editorRect.right - Math.min(60, editorRect.width / 4); return originalPosAtCoords({ left: probeX, top: coords.top }); } } return res; }; viewRef.current = editorView; setView(editorView); return () => { editorView.destroy(); viewRef.current = null; }; }, []); // Effect to update editable status without re-mounting useEffect(() => { if (viewRef.current) { viewRef.current.setProps({ editable: () => editable }); } }, [editable]); return { state, view, editorRef }; }