prosemirror-image-plugin

By Viktor Váczi
at Emergence Engineering
Try it out at https://emergence-engineering.com/blog/prosemirror-image-plugin
Features
- Drag and drop or paste images from anywhere
- Upload images to endpoints, showing placeholder until the upload finishes, and optionally delete
images when the image is removed from the document
- Customizable overlay for alignment ( or whatever you think of! )
- Optional image title
- Image resizing with body resize listeners, so the image always fits the editor ( inspired by czi-prosemirror )
- Scaling images with editor size ( when resizing is enabled )
- Easy to implement image caching ( in downloadPlaceholder )
How to use
import {schema} from "prosemirror-schema-basic";
import {EditorState} from "prosemirror-state";
import {EditorView} from "prosemirror-view";
import {
defaultSettings,
updateImageNode,
imagePlugin,
} from "prosemirror-image-plugin";
import "prosemirror-image-plugin/dist/styles/common.css";
import "prosemirror-image-plugin/dist/styles/withResize.css";
import "prosemirror-image-plugin/dist/styles/sideResize.css";
const imageSettings = {...defaultSettings};
const imageSchema = new Schema({
nodes: updateImageNode(schema.spec.nodes, {
...imageSettings,
}),
marks: schema.spec.marks,
});
const initialDoc = {
content: [
{
content: [
{
text: "Start typing!",
type: "text",
},
],
type: "paragraph",
},
],
type: "doc",
};
const state = EditorState.create({
doc: imageSchema.nodeFromJSON(initialDoc),
plugins: [
...exampleSetup({
schema: imageSchema,
}),
imagePlugin({...imageSettings}),
],
});
const view: EditorView = new EditorView(document.getElementById("editor"), {
state,
});
Configuration
ImagePluginSettings
Interface for the settings used by this plugin.
uploadFile | (file: File) => Promise<string> | Uploads the image file to a remote server and returns the uploaded image URL. By default it returns the dataURI of the image. |
deleteSrc | (src: string) => Promise<void> | Deletes the image from the server. |
hasTitle | boolean | If set to true then the image has a title field. True by default. isBlock should be true if set. |
extraAttributes | Record<string, string | null> | Extra attributes on the new image node. By default is defaultExtraAttributes . |
createOverlay | ( node: PMNode, getPos: (() => number) | boolean, view: EditorView) => Node | undefined | create an overlay DOM Node for the image node. The default is the one you see in the intro image. |
updateOverlay | ( overlayRoot: Node, getPos: (() => number) | boolean, view: EditorView, node: PMNode) => void | The function that runs whenever the image ProseMirror node changes to update the overlay. |
defaultTitle | string | Default title on new images. |
defaultAlt | string | Default alt on new images ( when it's not defined ). |
downloadImage | (url: string) => Promise<string> | Download image data with a callback function. Useful for images with behind auth. |
downloadPlaceholder | (url: string, view: EditorView) => string | {src?: string, className?: string} | If downloadImage is defined then this image is showed while the download is in progress. Caching can be done here if necessary. You can also apply a custom class while the download is in progress. |
isBlock | boolean | true if you want block images, false if you want inline ( ProseMirror default ). Titles are only possible with block images. Default true . |
enableResize | boolean | Enables resize features. Default true . |
resizeCallback | (el: Element, updateCallback: () => void) => () => void | Creates & destroys resize listeners |
imageMargin | number | Space in px on the side an image. Default 50. |
minSize | number | Minimum size in px of an image. Default 50. |
maxSize | number | Maximum size in px of an image. Default 2000. |
scaleImage | boolean | If true then images scale proportionally with editor width. Default true . |
createDecorations | (state: EditorState) => DecorationSet | Generate decorations from plugin state. Needed with YJS. |
createState | (pluginSettings: ImagePluginSetting) => StateField | Handle editor state differently. Needed with YJS. |
updateImageNode
Returns the updated nodes ( Schema["spec"]["nodes"] type
)
Arguments:
1 | nodes | Schema ["spec"] ["nodes"] | nodes from the to-be-updated Schema spec |
2 | pluginSettings | ImagePluginSettings | same plugin settings the plugin will be initialized with |
startImageUpload
Dispatches a transaction in the editor view which starts the image upload process ( and places placeholder etc ).
Returns undefined
Arguments:
1 | view | EditorView | Reference of the mounted editor view |
2 | file | File | image file to be uploaded |
3 | alt | string | alt of the file ( file.name usually works ) |
4 | pluginSettings | ImagePluginSettings | same plugin settings the plugin was initialized with |
5 | schema | Schema | updated schema used by the editor |
6 | pos | number | insert position in the document |
startImageUploadFn
Dispatches a transaction in the editor view which starts the image upload process ( and places placeholder etc ).
Returns Promise<ImageUploadReturn>
(as the uploadFile function you gave in but after the editor transformations)
Type helper; type ImageUploadReturn = { url: string; alt?: string };
Arguments:
1 | view | EditorView | Reference of the mounted editor view |
2 | uploadFile | () => Promise | An async function which returns the uploaded image src (and alt) |
3 | pos | number? | insert position in the document (defaults to start of selection) |
Example call;
const myImgUploadFn = (imgUrl: string) => async () => {
const file = await getFileFromtheInternet(imgUrl);
const src = await uploadFileToOurServer(file);
return { url: src, alt: file.name };
};
await startImageUploadFn(editor, myImgUploadFn(inputUrl));
Uploading files
Be aware that the default uploadFile
inserts the dataURI of the image directly into the
ProseMirror document. That can cause issues with large files, for ex. gif
s with long animations.
Upload placeholder
The plugin creates a widget decoration while the upload process is still in progress. The widget decoration's
dom node is a <placeholder>
, an example style could be:
placeholder {
color: #ccc;
position: relative;
top: 6px;
}
placeholder:after {
content: "☁";
font-size: 200%;
line-height: 0.1;
font-weight: bold;
}
Uploading images from a file picker
A small React example
In the "html" / JSX part:
<input type="file" id="imageselector" onChange={onInputChange}/>
The onInputChange
callback:
const onInputChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
if (
pmView?.state.selection.$from.parent.inlineContent &&
e.target.files?.length
) {
const file = e.target.files[0];
startImageUpload(
pmView,
file,
file.name,
defaultSettings,
imageSchema,
pmView.state.selection.from
);
}
},
[pmView]
);
CSS & Styles
The following styles are in the bundle:
import "prosemirror-image-plugin/dist/styles/common.css";
import "prosemirror-image-plugin/dist/styles/withResize.css";
import "prosemirror-image-plugin/dist/styles/sideResize.css";
import "prosemirror-image-plugin/dist/styles/withoutResize.css";
YJS compatibility
- BREAKING CHANGE: works with yjs collab out of the box, we have a custom mapping which deals with yjs replacing the
whole document on external changes, so no worries about lost decorations
Known issues
- titles and inline nodes do not work well together. If
hasTitle
is true then
isBlock
should also be true.
Development
Running & linking locally
- install plugin dependencies:
npm install
- install peer dependencies:
npm run install-peers
- link local lib:
npm run link
- link the package from the project you want to use it:
npm run link prosemirror-image-plugin
About us
Emergence Engineering is dev shop from the EU:
https://emergence-engineering.com/
We're looking for work, especially with ProseMirror ;)
Feel free to contact me at
viktor.vaczi@emergence-engineering.com