Skip to content

Commit a91fbce

Browse files
committed
Add inflate support
1 parent be47482 commit a91fbce

7 files changed

Lines changed: 150 additions & 38 deletions

File tree

.yarn/versions/3ea6144b.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/src/components/demos/shuffle.css

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,17 @@ img {
5252
.card.shuffle-hover-block {
5353
border: 1px solid lightblue;
5454
}
55+
56+
.inflatable-menu {
57+
display: flex;
58+
gap: 0.5rem;
59+
padding: 0.5rem;
60+
justify-content: start;
61+
}
62+
63+
.card-deck-inflatable {
64+
background-color: #f3efea;
65+
color: #351900;
66+
padding: 1rem;
67+
border-radius: 0.25rem;
68+
}

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

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -154,13 +154,20 @@ const editorState = EditorState.create({
154154

155155
export function ShuffleDemo() {
156156
return (
157-
<ProseMirror defaultState={editorState} nodeViewComponents={nodeViewComponents}>
158-
<ShuffleSkeleton>
159-
<ProseMirrorDoc />
160-
<ResizeHandles />
161-
<DragHandles handleComponent={CustomHandle} />
162-
</ShuffleSkeleton>
163-
</ProseMirror>
157+
<div>
158+
<div className="inflatable-menu">
159+
<div data-shuffle-inflatable="card_deck" className="card-deck-inflatable">
160+
Card deck
161+
</div>
162+
</div>
163+
<ProseMirror defaultState={editorState} nodeViewComponents={nodeViewComponents}>
164+
<ShuffleSkeleton>
165+
<ProseMirrorDoc />
166+
<ResizeHandles />
167+
<DragHandles handleComponent={CustomHandle} />
168+
</ShuffleSkeleton>
169+
</ProseMirror>
170+
</div>
164171
);
165172
}
166173

packages/shuffle/src/plugin.ts

Lines changed: 61 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { randomRef } from "@pitter-patter/refs";
99

1010
import { isShuffleRow, supportsDrag, supportsResize } from "./schema";
1111
import { autogroup } from "./transform/autogroup";
12+
import { inflate } from "./transform/inflate.js";
1213
import { reorder } from "./transform/reorder";
1314
import { reposition } from "./transform/reposition";
1415
import { TranslateCalculator } from "./translation.js";
@@ -277,6 +278,28 @@ export function shuffle({ hoverDecorations }: ShufflePluginOptions = {}) {
277278
},
278279
},
279280
},
281+
view(view) {
282+
function handleInflatePointerDown(event: PointerEvent) {
283+
if (!view.editable) return;
284+
if (!(event.target instanceof HTMLElement)) return;
285+
286+
if (view.dom.contains(event.target)) return;
287+
288+
const draggable = event.target.closest("[data-shuffle-inflatable]");
289+
290+
if (!(draggable instanceof HTMLElement)) return;
291+
292+
startDragOnPointerDown(view, null, draggable, event.clientX, event.clientY);
293+
}
294+
295+
view.root.addEventListener("pointerdown", handleInflatePointerDown as EventListener);
296+
297+
return {
298+
destroy() {
299+
view.root.removeEventListener("pointerdown", handleInflatePointerDown as EventListener);
300+
},
301+
};
302+
},
280303
});
281304
}
282305

@@ -293,7 +316,7 @@ interface NodeViewDesc {
293316

294317
export function startDragOnPointerDown(
295318
view: EditorView,
296-
pos: number,
319+
pos: number | null,
297320
dom: HTMLElement & { pmViewDesc?: ViewDesc },
298321
clientX: number,
299322
clientY: number,
@@ -329,16 +352,19 @@ export function startDragOnPointerDown(
329352
let layout: AutoLayout | null = null;
330353
let currentAnimation: Timeline | null = null;
331354
let skeletonOn = false;
355+
332356
const onMove = throttle(function onMove(e: PointerEvent) {
333357
if (!skeletonOn || !clone || !initialStyles || !layout) {
334358
const startResult = startDrag(dom!, translateCalc);
335359

336-
view.dispatch(
337-
view.state.tr.setMeta(shufflePluginKey, {
338-
type: "map",
339-
payload: { newPos: pos },
340-
}),
341-
);
360+
if (pos !== null) {
361+
view.dispatch(
362+
view.state.tr.setMeta(shufflePluginKey, {
363+
type: "map",
364+
payload: { newPos: pos },
365+
} satisfies ShufflePluginMeta),
366+
);
367+
}
342368

343369
clone = startResult.clone;
344370
clone.dataset["shuffleClone"] = "true";
@@ -362,14 +388,13 @@ export function startDragOnPointerDown(
362388

363389
const before = shufflePluginKey.getState(view.state)?.activeNodePos;
364390

365-
if (before === undefined) return;
366-
367391
if (currentAnimation?.began && !currentAnimation.completed) return;
368392

369-
const tr =
370-
autogroup(view, before, e.clientX, e.clientY) ??
371-
reorder(view, before, e.clientX, e.clientY) ??
372-
reposition(view, before, clone!.getBoundingClientRect());
393+
const tr = before
394+
? (autogroup(view, before, e.clientX, e.clientY) ??
395+
reorder(view, before, e.clientX, e.clientY) ??
396+
reposition(view, before, clone.getBoundingClientRect()))
397+
: inflate(view, clone, e.clientX, e.clientY);
373398

374399
if (!tr) return;
375400

@@ -389,6 +414,26 @@ export function startDragOnPointerDown(
389414
dom.dataset["shuffleActive"] = "true";
390415
});
391416

417+
function endDrag(domRect?: DOMRect) {
418+
if (!clone || !initialStyles) return;
419+
420+
clone.style.transition = "transform 0.2s ease";
421+
clone.style.boxShadow = initialStyles.boxShadow;
422+
423+
const { transform, transformOrigin } = domRect
424+
? translateCalc.place(domRect.left, domRect.top)
425+
: translateCalc.reset();
426+
427+
clone.style.transform = transform;
428+
clone.style.transformOrigin = transformOrigin;
429+
430+
setTimeout(() => {
431+
clone!.style.transition = "none";
432+
delete dom.dataset["shuffleActive"];
433+
clone!.remove();
434+
}, 250);
435+
}
436+
392437
function onUp() {
393438
document.removeEventListener("mousedown", preventSelection);
394439
document.removeEventListener("mousedown", preventSelection);
@@ -410,34 +455,20 @@ export function startDragOnPointerDown(
410455

411456
const before = shufflePluginKey.getState(view.state)?.activeNodePos;
412457

413-
if (before === undefined) return;
458+
if (before === undefined) return endDrag();
414459

415460
const dom = view.nodeDOM(before);
416-
if (!(dom instanceof HTMLElement)) return;
461+
if (!(dom instanceof HTMLElement)) return endDrag();
417462

418463
view.dispatch(
419464
view.state.tr.setMeta(shufflePluginKey, {
420465
type: "end",
421466
} satisfies ShufflePluginMeta),
422467
);
423468

424-
clone.style.transition = "transform 0.2s ease";
425-
clone.style.boxShadow = initialStyles.boxShadow;
426-
427469
const domRect = dom.getBoundingClientRect();
428470

429-
const { transform, transformOrigin } = translateCalc.place(domRect.left, domRect.top);
430-
431-
clone.style.transform = transform;
432-
clone.style.transformOrigin = transformOrigin;
433-
434-
setTimeout(() => {
435-
clone!.style.transition = "none";
436-
delete dom.dataset["shuffleActive"];
437-
clone!.remove();
438-
}, 250);
439-
440-
return;
471+
return endDrag(domRect);
441472
}
442473

443474
const preventSelection = (e: MouseEvent) => {
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { EditorView } from "prosemirror-view";
2+
3+
import { shufflePluginKey } from "../plugin.js";
4+
5+
import { findGap } from "./reorder.js";
6+
7+
export function inflate(view: EditorView, clone: HTMLElement, clientX: number, clientY: number) {
8+
const editorRect = view.dom.getBoundingClientRect();
9+
10+
if (
11+
clientX < editorRect.left ||
12+
clientX > editorRect.right ||
13+
clientY < editorRect.top ||
14+
clientY > editorRect.bottom
15+
) {
16+
return null;
17+
}
18+
19+
const nodeTypeName = clone.dataset["shuffleInflatable"];
20+
21+
if (!nodeTypeName) return null;
22+
23+
const nodeType = view.state.schema.nodes[nodeTypeName];
24+
25+
if (!nodeType) return null;
26+
27+
const posResult = view.posAtCoords({ left: clientX, top: clientY });
28+
if (!posResult) return null;
29+
30+
const { pos } = posResult;
31+
32+
const gap = findGap(view.state.doc, pos, nodeType);
33+
if (gap === null) return null;
34+
35+
const node = nodeType.createAndFill();
36+
37+
if (!node) return null;
38+
39+
const tr = view.state.tr;
40+
41+
tr.insert(gap, node);
42+
43+
tr.setMeta(shufflePluginKey, {
44+
type: "map",
45+
payload: { newPos: gap },
46+
});
47+
48+
tr.setMeta("composition", shufflePluginKey.getState(view.state)?.comp);
49+
50+
return tr;
51+
}

packages/shuffle/src/transform/reorder.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export function reorder(
5858
return tr;
5959
}
6060

61-
function findGap(doc: Node, pos: number, nodeType: NodeType) {
61+
export function findGap(doc: Node, pos: number, nodeType: NodeType) {
6262
const $pos = doc.resolve(pos);
6363

6464
let d = $pos.depth;

packages/shuffle/src/translation.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,11 @@ export class TranslateCalculator {
2929
transformOrigin: "initial",
3030
};
3131
}
32+
33+
reset() {
34+
return {
35+
transform: `rotateX(0) scale(1) translate(${this.originX}px, ${this.originY}px)`,
36+
transformOrigin: "initial",
37+
};
38+
}
3239
}

0 commit comments

Comments
 (0)