Skip to content

Commit 7fd8e87

Browse files
authored
Shuffle demo content (#33)
* Flesh out shuffle demo content * Handle inflation at pos 0 and select node on pointer down
1 parent 0b79b18 commit 7fd8e87

11 files changed

Lines changed: 196 additions & 26 deletions
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:19f2d0bc5f9e6650e13e97d966637dc667d71eb10d09faa014f25008664e3514
3+
size 71617
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:177d819e3bd9884d4110680dd9bc97c82c6dad3eea6af40b7cb2551256c6ecee
3+
size 895850
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:fb9267bd75f5b0e928ffd9706018cba8ff6e95cc6a3a6c4cee446caa940282b1
3+
size 29683

.yarn/versions/ef3e6fef.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
releases:
2+
"@pitter-patter/shuffle": patch

packages/docs/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"fumadocs-ui": "16.8.7",
2626
"lucide-react": "^1.14.0",
2727
"prosemirror-commands": "^1.7.1",
28+
"prosemirror-history": "^1.5.0",
2829
"prosemirror-keymap": "^1.2.3",
2930
"prosemirror-model": "^1.25.4",
3031
"prosemirror-schema-basic": "^1.2.4",
29.3 KB
Loading
21.5 KB
Loading

packages/docs/src/components/demos/shuffle.css

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
color: #351900;
55
padding-top: 2rem;
66
min-height: 800px;
7+
8+
--tw-prose-headings: #351900;
79
}
810

911
img {
@@ -58,11 +60,15 @@ img {
5860
gap: 0.5rem;
5961
padding: 0.5rem;
6062
justify-content: start;
63+
position: sticky;
64+
top: 0;
65+
z-index: 10000;
6166
}
6267

63-
.card-deck-inflatable {
68+
.inflatable {
6469
background-color: #f3efea;
6570
color: #351900;
6671
padding: 1rem;
6772
border-radius: 0.25rem;
73+
border: #351900 solid thin;
6874
}

packages/docs/src/components/demos/shuffle.tsx

Lines changed: 127 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import {
77
reactKeys,
88
useSelectNode,
99
} from "@handlewithcare/react-prosemirror";
10-
import { baseKeymap } from "prosemirror-commands";
10+
import { baseKeymap, toggleMark } from "prosemirror-commands";
11+
import { history, undo, redo } from "prosemirror-history";
1112
import { keymap } from "prosemirror-keymap";
1213
import { Schema } from "prosemirror-model";
1314
import { schema as basic } from "prosemirror-schema-basic";
@@ -81,25 +82,91 @@ const schema = addShuffleNodes(
8182
);
8283

8384
const doc = schema.nodes.doc.create(null, [
84-
schema.nodes.row.create({ shuffleStart: 1, shuffleEnd: 12 }, [
85+
schema.nodes.heading.create({ level: 2 }, schema.text("Shuffle Demo")),
86+
schema.nodes.paragraph.create(
87+
null,
88+
schema.text(
89+
"Shuffle is a ProseMirror plugin that supports smooth, grid-based reordering, auto-grouping, repositioning, and resizing. Drag this paragraph to see it in action.",
90+
),
91+
),
92+
schema.nodes.paragraph.create(
93+
null,
94+
schema.text(
95+
"ProseMirror has built-in drag-and-drop behavior, but it's limited in functionality and relies on the browser’s native “ghosted” drag thumbnails. Shuffle focuses on clarity: users should be able to see how their actions are affecting their document in real time, and know exactly what the document will look like when they drop.",
96+
),
97+
),
98+
schema.nodes.heading.create({ level: 3 }, schema.text("Reordering")),
99+
schema.nodes.paragraph.create(
100+
null,
101+
schema.text(
102+
"Use Shuffle to drag ProseMirror nodes around the document, automatically swapping positions with other node views, sinking and lifting to arbitrary depths as needed, and giving constant feedback to the user about how their document is changing.",
103+
),
104+
),
105+
schema.nodes.container.create({ shuffleStart: 2, shuffleEnd: 11 }, [
106+
schema.nodes.heading.create({ level: 3 }, schema.text("Resizing")),
107+
schema.nodes.paragraph.create(
108+
null,
109+
schema.text(
110+
"Shuffle divides the editor width into 12 equal columns. Blocks snap to column boundaries when resized or repositioned. Select any block and drag its handles to resize it.",
111+
),
112+
),
113+
]),
114+
schema.nodes.heading.create({ level: 3 }, schema.text("Rows and containers")),
115+
schema.nodes.paragraph.create(
116+
null,
117+
schema.text(
118+
"Rows are horizontal groupings of block nodes. Drag a block next to another and Shuffle wraps them in a row automatically. Drag the last block out and the row disappears.",
119+
),
120+
),
121+
schema.nodes.paragraph.create(
122+
null,
123+
schema.text(
124+
"Containers are an optional vertical grouping of block nodes that behave similarly to rows. Blocks inside of it can be aligned. A container persists after you remove the blocks inside of it.",
125+
),
126+
),
127+
schema.nodes.row.create(null, [
85128
schema.nodes.image.create({
86129
shuffleStart: 1,
87130
shuffleEnd: 5,
88-
src: "https://t4.ftcdn.net/jpg/02/71/88/53/360_F_271885326_Jkc8UkWTYmgB3dJjhrot2QZEiLneCaaM.jpg",
131+
src: "/images/shuffle-cards.jpg",
89132
}),
90133
schema.nodes.container.create({ shuffleStart: 7, shuffleEnd: 12 }, [
91-
schema.nodes.paragraph.create(null, [schema.text("This is some sample text")]),
92-
schema.nodes.paragraph.create(null, [schema.text("This is some more sample text")]),
134+
schema.nodes.paragraph.create(null, [
135+
schema.text(
136+
"When you drag a block beside another, Shuffle can automatically create a row. Drag everything out of the row and the row dissolves.",
137+
),
138+
]),
139+
schema.nodes.paragraph.create(null, [
140+
schema.text(
141+
"Within a row, blocks can be repositioned independently. Try adjusting the horizontal position of the blocks in this row.",
142+
),
143+
]),
93144
]),
94145
]),
95-
schema.nodes.image.create({
96-
src: "https://t4.ftcdn.net/jpg/02/71/88/53/360_F_271885326_Jkc8UkWTYmgB3dJjhrot2QZEiLneCaaM.jpg",
97-
}),
98-
schema.nodes.paragraph.create(null, schema.text("Another paragraph not in a row.")),
146+
schema.nodes.row.create(null, [
147+
schema.nodes.paragraph.create({ shuffleStart: 3, shuffleEnd: 7, zIndex: 2 }, [
148+
schema.text(
149+
"When blocks overlap, the last one you touched sits on top. You can drag and drop the other block in place to bring it forward.",
150+
),
151+
]),
152+
schema.nodes.image.create({
153+
shuffleStart: 6,
154+
shuffleEnd: 10,
155+
zIndex: 1,
156+
src: "/images/shuffle-dance.jpg",
157+
}),
158+
]),
159+
schema.nodes.heading.create({ level: 3 }, schema.text("Containment")),
160+
schema.nodes.paragraph.create(
161+
null,
162+
schema.text(
163+
"The card deck below uses containment - its cards can be dragged and reordered within the deck, but they can’t be moved outside of it. The schema defines what is allowed and Shuffle enforces it automatically.",
164+
),
165+
),
99166
schema.nodes.card_deck.create({ shuffleStart: 2, shuffleEnd: 11 }, [
100167
schema.nodes.card.create(null, [
101168
schema.nodes.paragraph.create(null, [
102-
schema.text("You can configure containment on node types, too."),
169+
schema.text("You can configure containment on node types."),
103170
]),
104171
]),
105172
schema.nodes.card.create(null, [
@@ -113,6 +180,13 @@ const doc = schema.nodes.doc.create(null, [
113180
]),
114181
]),
115182
]),
183+
schema.nodes.heading.create({ level: 3 }, schema.text("Inflate")),
184+
schema.nodes.paragraph.create(
185+
null,
186+
schema.text(
187+
"New blocks can be dragged into the document directly from the menu. Grab any component from the panel and drop it where you want.",
188+
),
189+
),
116190
]);
117191

118192
function Image({ nodeProps, ref, children: _, ...props }: NodeViewComponentProps) {
@@ -148,13 +222,29 @@ const editorState = EditorState.create({
148222
shuffle({
149223
hoverDecorations,
150224
}),
151-
keymap(baseKeymap),
225+
history(),
152226
],
153227
});
154228

229+
const plugins = [
230+
keymap({
231+
...baseKeymap,
232+
"Mod-i": toggleMark(schema.marks.em),
233+
"Mod-b": toggleMark(schema.marks.strong),
234+
"Mod-Shift-c": toggleMark(schema.marks.code),
235+
"Mod-z": undo,
236+
"Mod-y": redo,
237+
"Mod-Shift-z": redo,
238+
}),
239+
];
240+
155241
export function ShuffleDemo() {
156242
return (
157243
<div>
244+
<p>
245+
Drag one of the menu items below into the document to automatically create a node of that
246+
type!
247+
</p>
158248
<div className="inflatable-menu">
159249
<div
160250
data-shuffle-inflatable={JSON.stringify(
@@ -176,12 +266,36 @@ export function ShuffleDemo() {
176266
])
177267
.toJSON(),
178268
)}
179-
className="card-deck-inflatable"
269+
className="inflatable"
180270
>
181271
Card deck
182272
</div>
273+
<div
274+
data-shuffle-inflatable={JSON.stringify(
275+
schema.nodes.image
276+
.create({
277+
src: "/images/shuffle-dance.jpg",
278+
})
279+
.toJSON(),
280+
)}
281+
className="inflatable"
282+
>
283+
Image
284+
</div>
285+
<div
286+
data-shuffle-inflatable={JSON.stringify(
287+
schema.nodes.paragraph.create(null, schema.text("A brand new paragraph!")).toJSON(),
288+
)}
289+
className="inflatable"
290+
>
291+
Paragraph
292+
</div>
183293
</div>
184-
<ProseMirror defaultState={editorState} nodeViewComponents={nodeViewComponents}>
294+
<ProseMirror
295+
defaultState={editorState}
296+
nodeViewComponents={nodeViewComponents}
297+
plugins={plugins}
298+
>
185299
<ShuffleSkeleton>
186300
<ProseMirrorDoc />
187301
<ResizeHandles />

packages/shuffle/src/plugin.ts

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { AutoLayout, createLayout, Timeline } from "animejs";
22
import { animate } from "motion/mini";
33
import { Node, Node as PmNode } from "prosemirror-model";
4-
import { Plugin, PluginKey } from "prosemirror-state";
4+
import { NodeSelection, Plugin, PluginKey } from "prosemirror-state";
55
import { Decoration, DecorationSet, EditorView } from "prosemirror-view";
66
import throttle from "raf-throttle";
77

@@ -332,11 +332,17 @@ export function startDragOnPointerDown(
332332
clientX: number,
333333
clientY: number,
334334
) {
335-
view.dispatch(
336-
view.state.tr.setMeta(shufflePluginKey, {
337-
type: "start",
338-
} satisfies ShufflePluginMeta),
339-
);
335+
const startTr = view.state.tr;
336+
337+
startTr.setMeta(shufflePluginKey, {
338+
type: "start",
339+
} satisfies ShufflePluginMeta);
340+
341+
if (pos !== null) {
342+
startTr.setSelection(NodeSelection.create(view.state.doc, pos));
343+
}
344+
345+
view.dispatch(startTr);
340346

341347
const domRect = dom.getBoundingClientRect();
342348

@@ -401,11 +407,12 @@ export function startDragOnPointerDown(
401407

402408
if (currentAnimation?.began && !currentAnimation.completed) return;
403409

404-
const tr = before
405-
? (autogroup(view, before, e.clientX, e.clientY) ??
406-
reorder(view, before, e.clientX, e.clientY) ??
407-
reposition(view, before, clone.getBoundingClientRect()))
408-
: inflate(view, clone, e.clientX, e.clientY);
410+
const tr =
411+
before != null
412+
? (autogroup(view, before, e.clientX, e.clientY) ??
413+
reorder(view, before, e.clientX, e.clientY) ??
414+
reposition(view, before, clone.getBoundingClientRect()))
415+
: inflate(view, clone, e.clientX, e.clientY);
409416

410417
if (!tr) return;
411418

0 commit comments

Comments
 (0)