I’ve been playing with NodeView today to try to understand how it would work in our editor (we have a few things that would be considered NodeViews I guess: signature, payment etc).
The example I was playing with is the figure
block, a picture with a caption that can be edited with two states:
- empty: shows a dropzone and can upload a file on click/drop
- active: has an active image, an editable caption without markup and some actions on the picture
What I got so far is the following:
The schema part:
figure: {
content: "inline*",
marks: "",
group: "block",
defining: true,
draggable: true,
selectable: true,
attrs: {
src: {default: null},
caption: {default: ""},
},
parseDOM: [
{
tag: "figure",
contentElement: "figcaption",
getAttrs(dom: any) {
let img = dom.querySelector("img");
return {src: img && img.parentNode === dom ? img.src : null};
}
},
{
tag: "img[src]",
getAttrs(dom: any) {
return {src: dom.src, caption: dom.alt};
}
}
],
toDOM(node: any) {
return ["figure", ["img", {src: node.attrs.src, alt: node.attrs.caption}], ["figcaption", 0]]
}
}
A basic version of the NodeView that allows “uploading” and shows caption:
import {Node} from "prosemirror-model";
import {EditorView, NodeView} from "prosemirror-view";
export default class FigureView implements NodeView {
dom: HTMLElement;
input: HTMLInputElement | null = null;
node: Node;
view: EditorView;
getPos: () => number;
src: string | null;
caption: string;
constructor(node: Node, view: EditorView, getPos: () => number) {
this.src = node.attrs.src;
this.caption = node.attrs.caption;
this.node = node;
this.view = view;
this.getPos = getPos;
this.dom = document.createElement("figure");
this.dom.draggable = true;
this.dom.classList.add("editor-figure");
if (!!this.src) {
this.createViewWithImage();
} else {
this.createEmptyView();
}
}
private createViewWithImage() {
this.dom.innerHTML = "";
this.dom.classList.remove("editor-figure--empty");
if (!this.src) {
return;
}
const img = document.createElement("img");
img.src = this.src;
this.dom.appendChild(img);
const caption = document.createElement("figcaption");
caption.innerText = this.caption;
caption.contentEditable = "true";
this.dom.appendChild(caption);
}
private createEmptyView() {
this.dom.innerHTML = "";
this.dom.classList.add("editor-figure--empty");
this.input = document.createElement("input");
this.input.type = "file";
let image = null;
this.input.onclick = () => {
image = null;
};
this.input.onchange = (e: any) => {
if (this.input && this.input.files && this.input.files[0]) {
const reader = new FileReader();
reader.onload = (e: any) => {
this.input = null;
this.src = e.target.result;
this.updateAttrs();
this.createViewWithImage();
this.uploadPic(this.src!);
};
reader.readAsDataURL(this.input.files[0]);
}
};
this.dom.addEventListener("click", (e: Event) => {
e.preventDefault();
if (this.input) {
this.input.click();
}
});
}
private updateAttrs() {
this.view.dispatch(
this.view.state.tr.setNodeMarkup(
this.getPos(),
undefined,
{src: this.src, caption: this.caption}
)
);
}
private uploadPic(file: string) {
// upload the picture using whatever lib and update the src
// to be the url instead of the base64
console.log("Uploading pic!");
// this.src = my_href
// document.querySelector("img").src = this.src;
}
stopEvent() {
return true;
}
}
This kinda works but there are some issues:
1/ Is there a way to make the figcaption
automatically part of the prosemirror editor somehow or is the recommended way to embed another instance of PM like in http://prosemirror.net/examples/footnote/ to avoid selection/undo/redo working?
2/ I set the figure
to be draggable in the schema and in the dom but I can’t move the node around in the editor. Did I miss something?