ProseMirror Resizable View
The `prosemirror-resizable-view` package provides a specialized `NodeView` implementation for ProseMirror that enables users to resize custom nodes directly within the editor interface. It is an integral part of the broader Remirror ecosystem and currently aligns with Remirror's stable version 3.0.0. This package itself is at version 3.0.0, benefiting from the active development cadence of the Remirror project, which includes regular patch releases for bug fixes and dependency updates. Its primary differentiator lies in abstracting the complexities of implementing resizable DOM elements within a ProseMirror `NodeView`, offering a convenient base class that handles drag events and dimension management. This significantly simplifies the development process for integrating dynamic resize functionality for various content types like images, video embeds, or custom block components. The package comes with comprehensive TypeScript type definitions, enhancing developer experience and code safety.
Common errors
-
TypeError: node.update is not a function
cause This error occurs when a custom `NodeView` (including one extending `ResizableNodeView`) does not correctly implement or return a value from its `update` method when the underlying ProseMirror node attributes change.fixEnsure your `ResizableNodeView` subclass implements an `update(node: ProsemirrorNode)` method that appropriately handles attribute changes and returns `true` if it can update the DOM in place, or `false` if ProseMirror should redraw the node view entirely. -
Cannot read properties of undefined (reading 'appendChild')
cause This typically indicates that the `dom` or `contentDOM` property of the `NodeView` (or the element returned by `createElement`) was not properly initialized or returned a `null`/`undefined` value, preventing ProseMirror from attaching children.fixVerify that your `ResizableNodeView` subclass's `createElement` method always returns a valid `HTMLElement`, and that the `dom` property of the `NodeView` instance is correctly assigned to this element. -
Resize handles are visible but do not respond to drag events.
cause This is often a CSS-related issue, where the resize handles' `pointer-events` property or their z-index prevents them from receiving mouse events, or event listeners are incorrectly attached.fixInspect the CSS of the resize handles to ensure `pointer-events` are not set to `none` (unless intended) and that they are positioned above other elements. Double-check that event listeners for resizing are correctly bound to the resize handle elements.
Warnings
- breaking As `prosemirror-resizable-view` aligns with the Remirror v3 ecosystem, projects upgrading to Remirror v3 will need to contend with its shift to Stage 3 decorators from TypeScript's experimental decorators. While this package itself may not directly expose decorators, other Remirror extensions commonly used alongside it require this update, potentially affecting TypeScript configurations.
- breaking Remirror v3, which this package is part of, has updated its underlying ProseMirror dependencies. This may necessitate users to verify that their core `prosemirror-*` package versions are compatible with the versions expected by the `prosemirror-resizable-view` package and its Remirror counterparts.
- gotcha Directly manipulating the DOM elements of a `NodeView` (e.g., changing width/height styles) without dispatching a ProseMirror transaction will often result in the changes being overwritten by ProseMirror's rendering cycle, leading to non-persistent visual updates.
- gotcha `ResizableNodeView` is an abstract class and cannot be instantiated directly. It requires extension by a custom class that implements the abstract `createElement` method to provide the actual DOM structure for the node.
- gotcha Implementing functional resize handles requires careful styling (CSS) for the overlay and interaction areas. Without correct `position`, `z-index`, and `pointer-events` properties, the handles may not be visible or respond to user drag events.
Install
-
npm install prosemirror-resizable-view -
yarn add prosemirror-resizable-view -
pnpm add prosemirror-resizable-view
Imports
- ResizableNodeView
const ResizableNodeView = require('prosemirror-resizable-view');import { ResizableNodeView } from 'prosemirror-resizable-view'; - Node
import { Node } from 'prosemirror-model';import { Node as ProsemirrorNode } from 'prosemirror-model'; - NodeView
import { NodeView } from 'prosemirror-model';import { EditorView, NodeView } from 'prosemirror-view'; - EditorView
import { EditorView } from '@remirror/pm';import { EditorView } from 'prosemirror-view';
Quickstart
import { ResizableNodeView } from 'prosemirror-resizable-view';
import { Node as ProsemirrorNode, Schema } from 'prosemirror-model';
import { EditorView, NodeView } from 'prosemirror-view';
import { EditorState } from 'prosemirror-state';
import { baseKeymap } from 'prosemirror-commands';
import { keymap } from 'prosemirror-keymap';
// 1. Define a basic schema with an 'image' node that can have width/height attributes
const mySchema = new Schema({
nodes: {
doc: { content: 'block+' },
paragraph: { content: 'inline*', group: 'block' },
image: {
inline: false,
attrs: {
src: { default: '' },
alt: { default: null },
title: { default: null },
width: { default: null },
height: { default: null },
},
group: 'block',
parseDOM: [{
tag: 'img[src]',
getAttrs(dom) {
if (typeof dom === 'string') return {};
return {
src: dom.getAttribute('src'),
alt: dom.getAttribute('alt'),
title: dom.getAttribute('title'),
width: dom.getAttribute('width'),
height: dom.getAttribute('height'),
};
},
}],
toDOM(node) {
return [
'img',
{
src: node.attrs.src,
alt: node.attrs.alt,
title: node.attrs.title,
width: node.attrs.width, // Render initial width
height: node.attrs.height, // Render initial height
},
];
},
},
text: { inline: true, group: 'inline' },
},
marks: {},
});
// 2. Helper function to create the actual DOM element for the image
const createInnerImage = ({ node }: { node: ProsemirrorNode }) => {
const inner = document.createElement('img');
inner.setAttribute('src', node.attrs.src);
if (node.attrs.alt) inner.setAttribute('alt', node.attrs.alt);
if (node.attrs.title) inner.setAttribute('title', node.attrs.title);
inner.style.width = node.attrs.width ? `${node.attrs.width}px` : '100%';
inner.style.height = node.attrs.height ? `${node.attrs.height}px` : 'auto';
inner.style.minWidth = '50px';
inner.style.objectFit = 'contain';
return inner;
};
// 3. Extend ResizableNodeView to create a custom resizable image view
export class ResizableImageView extends ResizableNodeView implements NodeView {
constructor(node: ProsemirrorNode, view: EditorView, getPos: () => number) {
super({
node,
view,
getPos,
createElement: createInnerImage,
// Optional: Set aspectRatio to 'lock' to maintain aspect ratio, or 'free'.
// aspectRatio: 'lock',
updateSize: (width, height) => {
// This callback is crucial: dispatch a transaction to update the node's attributes
const tr = view.state.tr.setNodeMarkup(getPos(), undefined, {
...node.attrs,
width,
height,
});
view.dispatch(tr);
},
});
}
}
// 4. Set up the ProseMirror Editor
const editorDiv = document.createElement('div');
editorDiv.id = 'editor';
document.body.appendChild(editorDiv);
const state = EditorState.create({
schema: mySchema,
doc: mySchema.nodeFromJSON({
type: 'doc',
content: [
{
type: 'paragraph',
content: [{ type: 'text', text: 'Drag the handles to resize the image:' }],
},
{
type: 'image',
attrs: { src: 'https://via.placeholder.com/250x180', width: 250, height: 180 },
},
{
type: 'paragraph',
content: [{ type: 'text', text: 'This is some text below the resizable image.' }],
},
],
}),
plugins: [keymap(baseKeymap)],
});
const view = new EditorView(editorDiv, {
state,
nodeViews: {
image(node, view, getPos) {
return new ResizableImageView(node, view, getPos);
},
},
});
// Expose view for debugging in browser console
(window as any).view = view;