How to get data from the editor?

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;

Did you try searching for existing react thread ? https://discuss.prosemirror.net/search?q=react, possibly this ticket

1 Like

Yes, I did and this code is actually extended version of that. Now I actually want to know how to get the data from the editor so that I can store.

And thank you for the reply.

Hi @powerranger I see that you are using lots of refs, and I can’t help but ask what is the reason you are using React for? As far as I understand React, it is great at keeping components in sync with a well-defined state. When you use refs, you are somewhat breaking that, so the React library is perhaps not serving its purpose.

Alternatively, you can change the implementation to having it controlled (passing the props value and onChange) but you will have to build an implementation for that. The question is what exactly you are searching for.

I tried to put your code on codesandbox.io but couldn’t get it to work 100%, perhaps you can edit it to make it work, and we can try from there, or you can make your requirement more clearly defined, and we can try coming up with a solution which is more appropriate, using React. (Please explain how the React parent component exchange information with this component, if they don’t - it can simply be a component which isn’t connected to the other React components, just a simple

block with some extra code to run the ProseMirror)

Here is the incomplete sandbox we can work on:

You can get the data from the editor by writing a simple plugin that is triggered whenever there is an update to the PM editor:

So here’s the plugin:

import {Plugin} from 'prosemirror-state';
import {updateFromView, getActiveMarks, getAvailableNodeTypes, getAvailableMarks, getHTMLStringFromState} from 'client/features/story-editor/prosemirror/utils.js';

export default (dispatchUpdateCallback) => {
	return new Plugin({
		view() {
			return {
				update (updatedView) {
					const json = updatedView.state.toJSON();
					dispatchUpdateCallback(json);
				}
			}
		}
	});
};

You need to pass a callback when adding the plugin in the editor state:

onUpdatePlugin(someCallback)

If you want HTML instead of JSON you can use this function to get it from an editor state:

export function getHTMLStringFromState (state) {
	const fragment = DOMSerializer.fromSchema(state.schema).serializeFragment(state.doc.content);
	const div = document.createElement("div");
	div.appendChild(fragment);
	return div.innerHTML;
}

Hello @iswara108 , thank you so much for the reply. Actually I want to add prosemirror editor that supports image resizing, auto saving after every 5 words, and the editor menu on tooltip. This is my requirement. And also just to mention, this is a part of Next.js application in javascript.

I created a project on sandbox -> https://codesandbox.io/s/editorprosemirror-oxt42 Your help means a lot to me. Thank you.

Hello @Pier Can you please help me with how can I use this in https://codesandbox.io/s/editorprosemirror-oxt42 . I want to save the content in HTML format after typing 5 words .

Thank you for your time.

Sorry, I took a look but there’s just too much React soup and I don’t have time to figure it out.

Use the update plugin and instead of getting JSON use the other code I posted to get HTML from the editor state.