Quick start
Install and configure Shuffle
npm i @pitter-patter/shuffle prosemirror-view@1.41.7Update your schema
Shuffle requires a few schema modifications in order to work as expected:
- A
rownode spec.rowis a block node that should allow other top level blocks as children. When a node is dragged alongside an existing node, they will be automatically wrapped in arowparent node, which allows them to live on the same grid row. - A
containernode spec.containeris an optional vertical grouping of block nodes. - A
pitterPatter.shuffleconfiguration for any existing node specs that should be draggable and/or resizable.
Shuffle can automatically extend your schema for you, or you can modify your schema yourself to add Shuffle support.
To extend your schema automatically:
import { addShuffleNodes } from "@pitter-patter/shuffle";
// Adds row and container nodes to your schema with
// content: "block+", and configures each node with
// the group "block" to be resizable and draggable.
const shuffledSchema = addShuffleNodes(schema, "block+", "block");Or, to manually update your schema, just add the row and container nodes yourself, and configure nodes to be resizable and draggable as needed:
import { container, row, shuffleAttrs } from "@pitter-patter/shuffle";
const schema = new Schema({
nodes: {
doc: {
content: "block+",
},
text: {
group: "inline",
inline: true,
},
paragraph: {
group: "block",
content: "inline*",
attrs: {
...shuffleAttrs,
},
pitterPatter: {
shuffle: {
resizable: true,
draggablue: true,
},
},
},
row: {
...row,
group: "block",
content: "block+",
},
container: {
...container,
group: "block",
content: "block+",
},
},
});Add the plugin
Most of Shuffle’s logic lives in the shuffle() ProseMirror plugin. This should be added to your
EditorState:
import { reactKeys } from "@handlewithcare/react-prosemirror";
import { shuffle } from "@pitter-patter/shuffle";
const editorState = EditorState.create({
schema,
doce,
plugins: [reactKeys(), shuffle()],
});Configure hover decorations
In schemas that can have deeply nested nodes, it can be helpful to use borders or highlights to indicate to the user which nodes are being hovered over.
The hoverDecorations argument to the shuffle plugin creator will be called with each hovered node
to determine whether to render a node decoration.
import { reactKeys } from "@handlewithcare/react-prosemirror";
import { shuffle } from "@pitter-patter/shuffle";
import { Decoration } from "prosemirror-view";
import { Node } from "prosemirror-model";
function hoverDecorations(from: number, to: number, node: Node) {
// Return null to skip decorations for a given node
if (node.type.name === "image") return null;
return Decoration.node(from, to, {
class: "shuffle-hover-block",
});
}
const editorState = EditorState.create({
schema,
doc,
plugins: [
reactKeys(),
shuffle({
hoverDecorations,
}),
],
});Wrap your ProseMirror component with the ShuffleSkeleton and add ResizeHandles and DragHandles
Shuffle provides a ShuffleSkeleton component that wraps your ProseMirrorDoc. It renders
Shuffle's grid skeleton, and must be rendered for resize and reposition behaviors to work correctly.
The component should be a direct parent of the ProseMirrorDoc component.
To add resize handles to your elements, include the ResizeHandles component as a child of your
ShuffleSkeleton. Likewise, include the DragHandles component to render drag handles.
function Editor() {
return (
<ProseMirror defaultState={editorState}>
<ShuffleSkeleton>
<ProseMirrorDoc />
<ResizeHandles />
<DragHandles />
</ShuffleSkeleton>
</ProseMirror>
);
}Import the styles
Shuffle provides a small functional stylesheet for visualizing and positioning nodes on the grid.
import "@pitter-patter/shuffle/styles.css";Customizing
The appearance of the skeleton can be customized with CSS variables:
:root {
--shuffle-column-width: 3rem; /* The width of an individual grid column */
--shuffle-gutter-width: 1.5rem; /* The visual gap between grid columns in the skeleton */
--shuffle-row-gap: 1rem; /* The gap between rows in the grid */
--shuffle-skeleton-color: lightgray /* The color of the grid columns in the skeleton */;
}The ResizeHandles component optionally takes a handleComponent prop that will be used instead of
the default light blue button:
import { ResizeHandles } from "@pitter-patter/shuffle";
import { EventHandler, PointerDown } from "react";
interface Props {
style: { top: number; left: number };
onPointerDown: EventHandler<PointerDown>;
}
function ResizeHandle({ styles, onPointerDown }) {
return (
<button type="button" className="resize-handle" styles={styles} onPointerDown={onPointerDown} />
);
}
function Editor() {
return (
<ProseMirror defaultState={editorState}>
<ShuffleSkeleton>
<ProseMirrorDoc />
<ResizeHandles handleComponent={ResizeHandle} />
</ShuffleSkeleton>
</ProseMirror>
);
}Similarly, the DragHandles component optionally takes a handleComponent prop that will be used
instead of the default light blue button:
import { DragHandles } from "@pitter-patter/shuffle";
import { EventHandler, PointerDown } from "react";
interface Props {
style: { top: number; left: number };
onPointerDown: EventHandler<PointerDown>;
}
function DragHandle({ styles, onPointerDown, node }) {
return (
<button type="button" className="drag-handle" styles={styles} onPointerDown={onPointerDown}>
{node.type.name[0].toUpperCase() + node.type.name.slice(1)}
</button>
);
}
function Editor() {
return (
<ProseMirror defaultState={editorState}>
<ShuffleSkeleton>
<ProseMirrorDoc />
<DragHandles handleComponent={DragHandle} />
</ShuffleSkeleton>
</ProseMirror>
);
}