Shuffle

Quick start

Install and configure Shuffle

npm i @pitter-patter/shuffle prosemirror-view@1.41.7

Update your schema

Shuffle requires a few schema modifications in order to work as expected:

  • A row node spec. row is 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 a row parent node, which allows them to live on the same grid row.
  • A container node spec. container is an optional vertical grouping of block nodes.
  • A pitterPatter.shuffle configuration 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>
  );
}

On this page