Skip to content

Commit c682ef0

Browse files
authored
Fix autogrouping and reordering within existing rows (#59)
This fixes a few bugs that popped up in the previous round of bug fixes - We now have much better handling of reordering horizontally - We no longer block autogrouping after a previous drag auto-creates a row - We now auto-reorder after repositioning changes the visual order within a row - We now correctly handle reordering of nested blocks
1 parent 3efd2f0 commit c682ef0

6 files changed

Lines changed: 153 additions & 20 deletions

File tree

.yarn/versions/c07307f8.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/shuffle/src/plugin.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -451,9 +451,9 @@ export function startDragOnPointerDown(
451451

452452
const tr =
453453
before != null
454-
? (autogroup(view, before, x, y) ??
455-
reorder(view, before, x, y) ??
456-
reposition(view, before, clone.getBoundingClientRect()))
454+
? (reposition(view, before, clone.getBoundingClientRect()) ??
455+
autogroup(view, before, x, y) ??
456+
reorder(view, before, x, y))
457457
: inflate(view, clone, x, y);
458458

459459
if (!tr) return;

packages/shuffle/src/transform/autogroup.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export function autogroup(
1515
const rowType = getShuffleRowType(view.state.schema);
1616
if (!rowType) return null;
1717

18-
const node = view.state.doc.resolve(from).nodeAfter;
18+
const node = view.state.doc.nodeAt(from);
1919
if (!node) return null;
2020

2121
if (node.type.spec.pitterPatter?.shuffle?.containedBy) return null;

packages/shuffle/src/transform/inflate.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export function inflate(view: EditorView, clone: HTMLElement, clientX: number, c
2828

2929
const { pos } = posResult;
3030

31-
const gap = findGap(view.state.doc, pos, node.type);
31+
const gap = findGap(view, pos, node.type, null, clientX, clientY);
3232
if (gap === null) return null;
3333

3434
if (!node) return null;

packages/shuffle/src/transform/reorder.ts

Lines changed: 47 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { reactKeys } from "@handlewithcare/react-prosemirror";
2-
import { Node, NodeType } from "prosemirror-model";
2+
import { NodeType } from "prosemirror-model";
33
import { Transaction } from "prosemirror-state";
44
import { insertPoint } from "prosemirror-transform";
55
import { EditorView } from "prosemirror-view";
@@ -33,18 +33,20 @@ export function reorder(
3333
return null;
3434
}
3535

36-
const gap = findGap(view.state.doc, pos, node.type);
36+
if (pos <= from + node.nodeSize && pos >= from) return null;
37+
38+
const gap = findGap(view, pos, node.type, from, clientX, clientY);
3739

3840
if (gap === null) return null;
3941

40-
if (gap === from + node.nodeSize || gap === from) return null;
42+
if (gap <= from + node.nodeSize && gap >= from) return null;
4143

4244
const tr = view.state.tr;
4345
tr.delete(from, from + node.nodeSize);
4446

4547
const newPos = tr.mapping.map(gap);
4648

47-
tr.insert(tr.mapping.map(gap), node);
49+
tr.insert(newPos, node);
4850

4951
tr.setMeta(reactKeys().spec.key!, {
5052
overrides: { [from]: newPos },
@@ -58,26 +60,57 @@ export function reorder(
5860
return tr;
5961
}
6062

61-
export function findGap(doc: Node, pos: number, nodeType: NodeType) {
63+
export function findGap(
64+
view: EditorView,
65+
pos: number,
66+
nodeType: NodeType,
67+
from: number | null,
68+
clientX: number,
69+
clientY: number,
70+
) {
71+
const { doc } = view.state;
6272
const $pos = doc.resolve(pos);
6373

74+
if ($pos.nodeAfter && $pos.parent.canReplaceWith($pos.index(), $pos.index(), nodeType)) {
75+
return pos;
76+
}
77+
78+
if (
79+
$pos.parentOffset == $pos.parent.content.size &&
80+
$pos.parent.canReplaceWith($pos.index(), $pos.index(), nodeType)
81+
) {
82+
return pos;
83+
}
84+
6485
let d = $pos.depth;
6586
while (!$pos.node(d).isBlock && d > 0) {
6687
d--;
6788
}
6889

69-
if (d === 0) {
70-
return pos;
71-
}
90+
if (d === 0) return null;
91+
92+
const candidateStart = $pos.before(d);
93+
94+
const candidateDom = view.domAtPos(candidateStart, 1).node;
95+
if (!(candidateDom instanceof Element)) return null;
96+
97+
const candidateRect = candidateDom.getBoundingClientRect();
98+
99+
const fromDom = from === null ? from : view.domAtPos(from, 1).node;
100+
if (fromDom !== null && !(fromDom instanceof Element)) return null;
101+
102+
const fromRect = fromDom?.getBoundingClientRect();
72103

73-
const textblock = $pos.node(d);
74-
const textblockPos = $pos.before(d);
104+
const horizontal =
105+
fromRect && candidateRect.top <= fromRect.bottom && candidateRect.bottom >= fromRect.top;
75106

76-
const isInFirstHalf = pos <= textblockPos + textblock.nodeSize / 2;
107+
const isInFirstHalf = horizontal
108+
? clientX < (candidateRect.left + candidateRect.right) / 2
109+
: clientY < (candidateRect.top + candidateRect.bottom) / 2;
77110

78-
const start = isInFirstHalf ? textblockPos : $pos.after(d);
111+
const candidateGap = isInFirstHalf ? candidateStart : $pos.after(d);
79112

80-
const gap = start ? insertPoint(doc, start, nodeType) : start;
113+
if (candidateGap === 0) return 0;
81114

82-
return gap;
115+
return insertPoint(doc, candidateGap, nodeType);
83116
}

packages/shuffle/src/transform/reposition.ts

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
import { Transaction } from "prosemirror-state";
1+
import { reactKeys } from "@handlewithcare/react-prosemirror";
2+
import { EditorState, Transaction } from "prosemirror-state";
23
import { EditorView } from "prosemirror-view";
34

45
import { setShuffleColumns } from "../commands.ts";
6+
import { shufflePluginKey } from "../plugin.ts";
57

68
export function reposition(view: EditorView, before: number, rect: DOMRect): Transaction | null {
79
const gridWrapper = view.dom.closest("[data-shuffle-wrapper]");
@@ -25,6 +27,7 @@ export function reposition(view: EditorView, before: number, rect: DOMRect): Tra
2527
closestDistance = distance;
2628
}
2729
}
30+
2831
if (closestBar === null) return null;
2932

3033
const $before = view.state.doc.resolve(before);
@@ -52,5 +55,100 @@ export function reposition(view: EditorView, before: number, rect: DOMRect): Tra
5255
transaction = tr;
5356
});
5457

58+
const parentStart = $before.start();
59+
const beforeParent = parentStart - 1;
60+
61+
const parent = $before.parent;
62+
63+
if (parent.type.spec.pitterPatter?.shuffle?.role !== "row") {
64+
return transaction;
65+
}
66+
67+
// setShuffleColumns doesn't change any positions in the doc
68+
// so we can safely use a position from before the transaction
69+
const starts = transaction.doc
70+
.nodeAt(beforeParent)!
71+
.children!.map((child) => child.attrs["shuffleStart"]);
72+
const order = transaction.doc
73+
.nodeAt(beforeParent)!
74+
.children!.map((_, index) => index)
75+
.toSorted((a, b) => starts[a]! - starts[b]!);
76+
77+
reorderSiblingsOnTransaction(
78+
parentStart,
79+
order,
80+
transaction,
81+
view.state.apply(transaction),
82+
(tr) => {
83+
transaction = tr;
84+
},
85+
);
86+
87+
const newPos = transaction.getMeta(reactKeys().spec.key!)!.overrides[before];
88+
89+
transaction.setMeta(shufflePluginKey, {
90+
type: "map",
91+
payload: { newPos },
92+
});
93+
5594
return transaction;
5695
}
96+
97+
// This is copied from React ProseMirror. I don't
98+
// really feel like we should export this, as it's
99+
// not really a command, but in this case we do need
100+
// this version of it, not the command
101+
function reorderSiblingsOnTransaction(
102+
pos: number,
103+
order: number[],
104+
tr: Transaction,
105+
state: EditorState,
106+
dispatch: (tr: Transaction) => void,
107+
) {
108+
const orderLookup = order.reduce<number[]>((acc, oldIndex, newIndex) => {
109+
acc[oldIndex] = newIndex;
110+
return acc;
111+
}, []);
112+
const $pos = state.doc.resolve(pos);
113+
if ($pos.start() !== pos) {
114+
return false;
115+
}
116+
if (!dispatch) return true;
117+
const nodes = $pos.parent.children;
118+
const reordered = nodes
119+
.map((node, i) => [node, i] as const)
120+
.sort((param, param1) => {
121+
let [, a] = param,
122+
[, b] = param1;
123+
return orderLookup[a]! - orderLookup[b]!;
124+
})
125+
.map((param) => {
126+
let [node] = param;
127+
return node;
128+
});
129+
tr.replaceWith(pos, $pos.parent.content.size + pos, reordered);
130+
const meta: { overrides: Record<number, number> } = {
131+
overrides: {},
132+
};
133+
const oldPositions = [];
134+
let start = pos;
135+
for (const node of nodes) {
136+
oldPositions.push(start);
137+
start += node.nodeSize;
138+
}
139+
start = pos;
140+
const newPositions: number[] = [];
141+
for (let i = 0; i < reordered.length; i++) {
142+
const node = reordered[i];
143+
newPositions[order[i]!] = start;
144+
start += node!.nodeSize;
145+
}
146+
for (let i = 0; i < oldPositions.length; i++) {
147+
const oldPosition = oldPositions[i]!;
148+
const newPosition = newPositions[i]!;
149+
meta.overrides[oldPosition] = newPosition;
150+
}
151+
tr.setMeta(reactKeys().spec.key!, meta);
152+
dispatch(tr);
153+
return true;
154+
}

0 commit comments

Comments
 (0)