@firecms/core
Version:
Awesome Firebase/Firestore-based headless open-source CMS
125 lines (111 loc) • 4.65 kB
text/typescript
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
};
}