Hello everyone, I’m completely new to prosemirror and It is important for me to create an editor as react component and I chose prosemirror. But it in itself has many concepts to learn. I tried and somehow I created a class component but I know its crappy. The main point is to create a resizable image in the editor. Any reference is appreciated, The code I wrote is working. But I want to get the content so that I can save it and can display it somewher else. Please help me with this.
import React, { useEffect, useRef } from "react";
import { Decoration, DecorationSet } from 'prosemirror-view';
import { Schema, DOMParser } from "prosemirror-model";
import { EditorState, NodeSelection, PluginKey, Plugin } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import { schema, nodes } from "prosemirror-schema-basic";
import { exampleSetup } from "prosemirror-example-setup";
const reactPropsKey = new PluginKey("reactProps");
function reactProps(initialProps) {
return new Plugin({
key: reactPropsKey,
state: {
init: () => initialProps,
apply: (tr, prev) => tr.getMeta(reactPropsKey) || prev,
},
});
}
const resizableImage = {
inline: true,
attrs: {
src: {},
width: {default: "5em"},
alt: {default: null},
title: {default: null},
alignment: { default: "center" }
},
group: "inline",
draggable: true,
parseDOM: [{
priority: 51, // must be higher than the default image spec
tag: "img[src][width]",
getAttrs(dom) {
return {
src: dom.getAttribute("src"),
title: dom.getAttribute("title"),
alt: dom.getAttribute("alt"),
width: dom.getAttribute("width"),
alignment: dom.getAttribute("class")==="center" ? "center" : (dom.getAttribute("class")==="right" ? "right" : "left"),
}
}
}],
// TODO if we don't define toDom, something weird happens: dragging the image will not move it but clone it. Why?
toDOM(node) {
const attrs = {style: `width: ${node.attrs.width}`}
return ["div", { ...node.attrs, ...attrs }]
}
}
function getFontSize(element) {
return parseFloat(getComputedStyle(element).fontSize);
}
class FootnoteView {
constructor(node, view, getPos) {
const outer = document.createElement("div")
outer.style.position = "relative"
outer.style.width = node.attrs.width
//outer.style.border = "1px solid blue"
outer.style.display = "block"
//outer.style.paddingRight = "0.25em"
outer.style.lineHeight = "0"; // necessary so the bottom right arrow is aligned nicely
outer.style.marginLeft='auto';
outer.style.marginRight='auto';
const img = document.createElement("img")
img.setAttribute("src", node.attrs.src)
img.style.width = "100%"
//img.style.border = "1px solid red"
const handle = document.createElement("span")
handle.style.position = "absolute"
handle.style.bottom = "0px"
handle.style.right = "0px"
handle.style.width = "10px"
handle.style.height = "10px"
handle.style.border = "3px solid black"
handle.style.borderTop = "none"
handle.style.borderLeft = "none"
handle.style.display = "none"
handle.style.cursor = "nwse-resize"
handle.onmousedown = function(e) {
e.preventDefault()
const startX = e.pageX;
const startY = e.pageY;
const fontSize = getFontSize(outer)
const startWidth = parseFloat(node.attrs.width.match(/(.+)em/)[1])
const onMouseMove = (e) => {
const currentX = e.pageX;
const currentY = e.pageY;
const diffInPx = currentX - startX
const diffInEm = diffInPx / fontSize
outer.style.width = `${startWidth + diffInEm}em`
}
const onMouseUp = (e) => {
e.preventDefault()
document.removeEventListener("mousemove", onMouseMove)
document.removeEventListener("mouseup", onMouseUp)
let saveThisPos = getPos()
let transaction = view.state.tr.setNodeMarkup(getPos(), null, {src: node.attrs.src, width: outer.style.width} )
let resolvedPos = transaction.doc.resolve(saveThisPos)
let nodeSelection = new NodeSelection(resolvedPos)
transaction = transaction.setSelection(nodeSelection);
view.dispatch(transaction)
}
document.addEventListener("mousemove", onMouseMove)
document.addEventListener("mouseup", onMouseUp)
}
outer.appendChild(handle)
outer.appendChild(img)
this.dom = outer
this.img = img
this.handle = handle
}
selectNode() {
this.img.classList.add("ProseMirror-selectednode")
this.handle.style.display = ""
}
deselectNode() {
this.img.classList.remove("ProseMirror-selectednode")
this.handle.style.display = "none"
}
}
let placeholderPlugin = new Plugin({
state: {
init() { return DecorationSet.empty },
apply(tr, set) {
// Adjust decoration positions to changes made by the transaction
set = set.map(tr.mapping, tr.doc)
// See if the transaction adds or removes any placeholders
let action = tr.getMeta(this)
console.log("act",action)
if (action && action.add) {
let widget = document.createElement("placeholder")
let deco = Decoration.widget(action.add.pos, widget, {id: action.add.id})
set = set.add(tr.doc, [deco])
} else if (action && action.remove) {
set = set.remove(set.find(null, null,spec => spec.id == action.remove.id))
}
return set
}
},
props: {
decorations(state) { return this.getState(state) }
}
})
function findPlaceholder(state, id) {
let decos = placeholderPlugin.getState(state)
let found = decos.find(null, null, spec => spec.id == id)
return found.length ? found[0].from : null
}
const mySchema = new Schema({
nodes: { ...nodes, resizableImage },
marks: schema.spec.marks
})
function startImageUpload(view, file) {
// A fresh object to act as the ID for this upload
let id = {}
// Replace the selection with a placeholder
let tr = view.state.tr
if (!tr.selection.empty) tr.deleteSelection()
tr.setMeta(placeholderPlugin, {add: {id, pos: tr.selection.from }})
view.dispatch(tr)
uploadFile(file).then(url => {
let pos = findPlaceholder(view.state, id)
// If the content around the placeholder has been deleted, drop
// the image
if (pos == null) return
// Otherwise, insert it at the placeholder's position, and remove
// the placeholder
console.log(mySchema.nodes,"nodes")
view.dispatch(view.state.tr
.replaceWith(pos, pos, mySchema.nodes.resizableImage.create({src: 'https://cdn.vox-cdn.com/thumbor/PSLYCBn2BjUj8Zdbf4BD6SMus-0=/0x0:1800x1179/920x613/filters:focal(676x269:964x557):format(webp)/cdn.vox-cdn.com/uploads/chorus_image/image/66741310/3zlqxf_copy.0.jpg'}))
.setMeta(placeholderPlugin, {remove: {id}}))
}, () => {
// On failure, just clean up the placeholder
view.dispatch(tr.setMeta(placeholderPlugin, {remove: {id}}))
})
}
function uploadFile(file) {
let reader = new FileReader
return new Promise((accept, fail) => {
reader.onload = () => accept(reader.result)
reader.onerror = () => fail(reader.error)
// Some extra delay to make the asynchronicity visible
setTimeout(() => reader.readAsDataURL(file), 1500)
})
}
function Prosemirror(props) {
const editorRef = useRef();
const view = useRef(null);
const contentRef = useRef();
const imageUploadRef = useRef();
useEffect(() => { // initial render
const state = EditorState.create({
doc: DOMParser.fromSchema(mySchema).parse(contentRef.current),
plugins: exampleSetup({schema:mySchema}).concat([placeholderPlugin,reactProps(props)])
});
view.current = new EditorView(editorRef.current, {
state,
nodeViews: {
resizableImage(node, view, getPos) { return new FootnoteView(node, view, getPos) }
}
});
imageUploadRef.current.addEventListener("change", e => {
console.log("change")
if (view.current.state.selection.$from.parent.inlineContent && e.target.files.length)
startImageUpload(view.current, e.target.files[0])
view.current.focus()
})
imageUploadRef.current.addEventListener("click", e => e.target.value="")
return () => view.current.destroy();
}, []);
useEffect(() => {
console.log(view.current);
}, [view.current])
// useEffect(() => { // every render
// const tr = view.current.state.tr.setMeta(reactPropsKey, props);
// view.current.dispatch(tr);
// });
return (
<div>
<div>Insert Image: <input type="file" className="imageUpload" ref={imageUploadRef}/></div>
<div ref={editorRef} style={{width: "100%", height: "500px"}} >
</div>
<div ref={contentRef} style={{display: "none"}}>
<h1>
</h1>
<div className="test-div"></div>
<style jsx>
{`
placeholder {
display: inline;
border: 1px solid #ccc;
color: #ccc;
}
placeholder:after {
content: "☁";
font-size: 200%;
line-height: 0.1;
font-weight: bold;
}
`}
</style>
</div>
</div>
);
}
export default Prosemirror;