@blocknote/core 0.36.0 → 0.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/blocknote.cjs +7 -7
- package/dist/blocknote.cjs.map +1 -1
- package/dist/blocknote.js +1064 -999
- package/dist/blocknote.js.map +1 -1
- package/dist/style.css +1 -1
- package/dist/webpack-stats.json +1 -1
- package/package.json +2 -1
- package/src/api/__snapshots__/blocks-moved-down-twice-in-same-parent.json +44 -0
- package/src/api/__snapshots__/blocks-moved-insert-changes-sibling-order.json +26 -0
- package/src/api/__snapshots__/blocks-moved-nested-sibling-reorder.json +180 -0
- package/src/api/__snapshots__/blocks-moved-up-down-in-same-parent.json +44 -0
- package/src/api/__snapshots__/blocks-moved-up-down-in-same-transaction.json +44 -0
- package/src/api/{nodeUtil.test.ts → getBlocksChangedByTransaction.test.ts} +117 -1
- package/src/api/getBlocksChangedByTransaction.ts +422 -0
- package/src/api/nodeUtil.ts +0 -250
- package/src/blocks/FileBlockContent/helpers/render/createResizableFileBlockWrapper.ts +22 -0
- package/src/blocks/TableBlockContent/TableBlockContent.ts +26 -2
- package/src/editor/BlockNoteEditor.ts +1 -1
- package/src/editor/editor.css +5 -0
- package/src/extensions/BlockChange/BlockChangePlugin.ts +4 -2
- package/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts +1 -5
- package/src/index.ts +1 -0
- package/types/src/api/getBlocksChangedByTransaction.d.ts +63 -0
- package/types/src/api/nodeUtil.d.ts +0 -63
- package/types/src/editor/BlockNoteEditor.d.ts +1 -1
- package/types/src/extensions/BlockChange/BlockChangePlugin.d.ts +1 -1
- package/types/src/extensions/TableHandles/TableHandlesPlugin.d.ts +2 -2
- package/types/src/index.d.ts +1 -0
- package/types/src/schema/inlineContent/internal.d.ts +1 -1
- package/dist/tsconfig.tsbuildinfo +0 -1
- /package/types/src/api/{nodeUtil.test.d.ts → getBlocksChangedByTransaction.test.d.ts} +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it, beforeEach } from "vitest";
|
|
2
2
|
|
|
3
3
|
import { setupTestEnv } from "./blockManipulation/setupTestEnv.js";
|
|
4
|
-
import { getBlocksChangedByTransaction } from "./
|
|
4
|
+
import { getBlocksChangedByTransaction } from "./getBlocksChangedByTransaction.js";
|
|
5
5
|
import { BlockNoteEditor } from "../editor/BlockNoteEditor.js";
|
|
6
6
|
|
|
7
7
|
const getEditor = setupTestEnv();
|
|
@@ -452,4 +452,120 @@ describe("getBlocksChangedByTransaction", () => {
|
|
|
452
452
|
"__snapshots__/blocks-moved-multiple-in-same-transaction.json",
|
|
453
453
|
);
|
|
454
454
|
});
|
|
455
|
+
|
|
456
|
+
it("should return blocks which have been moved up or down in the same transaction", async () => {
|
|
457
|
+
editor.replaceBlocks(editor.document, [
|
|
458
|
+
{
|
|
459
|
+
id: "top",
|
|
460
|
+
type: "paragraph",
|
|
461
|
+
content: "Top",
|
|
462
|
+
},
|
|
463
|
+
{
|
|
464
|
+
id: "middle",
|
|
465
|
+
type: "paragraph",
|
|
466
|
+
content: "Middle",
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
id: "bottom",
|
|
470
|
+
type: "paragraph",
|
|
471
|
+
content: "Bottom",
|
|
472
|
+
},
|
|
473
|
+
]);
|
|
474
|
+
|
|
475
|
+
const blocksChanged = editor.transact((tr) => {
|
|
476
|
+
editor.setTextCursorPosition("top");
|
|
477
|
+
editor.moveBlocksDown();
|
|
478
|
+
|
|
479
|
+
return getBlocksChangedByTransaction(tr);
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
// Should report a single minimal move within the same parent
|
|
483
|
+
await expect(blocksChanged).toMatchFileSnapshot(
|
|
484
|
+
"__snapshots__/blocks-moved-up-down-in-same-transaction.json",
|
|
485
|
+
);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it("should detect moving the bottom block up within the same parent", async () => {
|
|
489
|
+
editor.replaceBlocks(editor.document, [
|
|
490
|
+
{ id: "top", type: "paragraph", content: "Top" },
|
|
491
|
+
{ id: "middle", type: "paragraph", content: "Middle" },
|
|
492
|
+
{ id: "bottom", type: "paragraph", content: "Bottom" },
|
|
493
|
+
]);
|
|
494
|
+
|
|
495
|
+
const blocksChanged = editor.transact((tr) => {
|
|
496
|
+
editor.setTextCursorPosition("bottom");
|
|
497
|
+
editor.moveBlocksUp();
|
|
498
|
+
return getBlocksChangedByTransaction(tr);
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
await expect(blocksChanged).toMatchFileSnapshot(
|
|
502
|
+
"__snapshots__/blocks-moved-up-down-in-same-parent.json",
|
|
503
|
+
);
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it("should detect moving a block down twice within the same parent as a single move", async () => {
|
|
507
|
+
editor.replaceBlocks(editor.document, [
|
|
508
|
+
{ id: "a", type: "paragraph", content: "A" },
|
|
509
|
+
{ id: "b", type: "paragraph", content: "B" },
|
|
510
|
+
{ id: "c", type: "paragraph", content: "C" },
|
|
511
|
+
]);
|
|
512
|
+
|
|
513
|
+
const blocksChanged = editor.transact((tr) => {
|
|
514
|
+
editor.setTextCursorPosition("a");
|
|
515
|
+
editor.moveBlocksDown();
|
|
516
|
+
editor.moveBlocksDown();
|
|
517
|
+
return getBlocksChangedByTransaction(tr);
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
await expect(blocksChanged).toMatchFileSnapshot(
|
|
521
|
+
"__snapshots__/blocks-moved-down-twice-in-same-parent.json",
|
|
522
|
+
);
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it("should detect nested sibling reorder within the same parent", async () => {
|
|
526
|
+
editor.replaceBlocks(editor.document, [
|
|
527
|
+
{
|
|
528
|
+
id: "parent",
|
|
529
|
+
type: "paragraph",
|
|
530
|
+
content: "Parent",
|
|
531
|
+
children: [
|
|
532
|
+
{ id: "child-a", type: "paragraph", content: "A" },
|
|
533
|
+
{ id: "child-b", type: "paragraph", content: "B" },
|
|
534
|
+
{ id: "child-c", type: "paragraph", content: "C" },
|
|
535
|
+
],
|
|
536
|
+
},
|
|
537
|
+
{ id: "sibling", type: "paragraph", content: "S" },
|
|
538
|
+
]);
|
|
539
|
+
|
|
540
|
+
const blocksChanged = editor.transact((tr) => {
|
|
541
|
+
editor.setTextCursorPosition("child-a");
|
|
542
|
+
editor.moveBlocksDown();
|
|
543
|
+
return getBlocksChangedByTransaction(tr);
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
await expect(blocksChanged).toMatchFileSnapshot(
|
|
547
|
+
"__snapshots__/blocks-moved-nested-sibling-reorder.json",
|
|
548
|
+
);
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
it("should not report moves when an insert changes sibling order", async () => {
|
|
552
|
+
editor.replaceBlocks(editor.document, [
|
|
553
|
+
{ id: "a", type: "paragraph", content: "A" },
|
|
554
|
+
{ id: "b", type: "paragraph", content: "B" },
|
|
555
|
+
{ id: "c", type: "paragraph", content: "C" },
|
|
556
|
+
]);
|
|
557
|
+
|
|
558
|
+
const blocksChanged = editor.transact((tr) => {
|
|
559
|
+
editor.insertBlocks(
|
|
560
|
+
[{ id: "x", type: "paragraph", content: "X" }],
|
|
561
|
+
"a",
|
|
562
|
+
"after",
|
|
563
|
+
);
|
|
564
|
+
return getBlocksChangedByTransaction(tr);
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
await expect(blocksChanged).toMatchFileSnapshot(
|
|
568
|
+
"__snapshots__/blocks-moved-insert-changes-sibling-order.json",
|
|
569
|
+
);
|
|
570
|
+
});
|
|
455
571
|
});
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import { combineTransactionSteps } from "@tiptap/core";
|
|
2
|
+
import deepEqual from "fast-deep-equal";
|
|
3
|
+
import type { Node } from "prosemirror-model";
|
|
4
|
+
import type { Transaction } from "prosemirror-state";
|
|
5
|
+
import {
|
|
6
|
+
Block,
|
|
7
|
+
DefaultBlockSchema,
|
|
8
|
+
DefaultInlineContentSchema,
|
|
9
|
+
DefaultStyleSchema,
|
|
10
|
+
} from "../blocks/defaultBlocks.js";
|
|
11
|
+
import type { BlockSchema } from "../schema/index.js";
|
|
12
|
+
import type { InlineContentSchema } from "../schema/inlineContent/types.js";
|
|
13
|
+
import type { StyleSchema } from "../schema/styles/types.js";
|
|
14
|
+
import { nodeToBlock } from "./nodeConversions/nodeToBlock.js";
|
|
15
|
+
import { isNodeBlock } from "./nodeUtil.js";
|
|
16
|
+
import { getPmSchema } from "./pmUtil.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Change detection utilities for BlockNote.
|
|
20
|
+
*
|
|
21
|
+
* High-level algorithm used by getBlocksChangedByTransaction:
|
|
22
|
+
* 1) Merge appended transactions into one document change.
|
|
23
|
+
* 2) Collect a snapshot of blocks before and after (flat map by id, and per-parent child order).
|
|
24
|
+
* 3) Emit inserts and deletes by diffing ids between snapshots.
|
|
25
|
+
* 4) For ids present in both snapshots:
|
|
26
|
+
* - If parentId changed, emit a move
|
|
27
|
+
* - Else if block changed (ignoring children), emit an update
|
|
28
|
+
* 5) Finally, detect same-parent sibling reorders by comparing child order per parent.
|
|
29
|
+
* We use an inlined O(n log n) LIS inside detectReorderedChildren to keep a
|
|
30
|
+
* longest already-ordered subsequence and mark only the remaining items as moved.
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
* Gets the parent block of a node, if it has one.
|
|
34
|
+
*/
|
|
35
|
+
function getParentBlockId(doc: Node, pos: number): string | undefined {
|
|
36
|
+
if (pos === 0) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
const resolvedPos = doc.resolve(pos);
|
|
40
|
+
for (let i = resolvedPos.depth; i > 0; i--) {
|
|
41
|
+
const parent = resolvedPos.node(i);
|
|
42
|
+
if (isNodeBlock(parent)) {
|
|
43
|
+
return parent.attrs.id;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* This attributes the changes to a specific source.
|
|
51
|
+
*/
|
|
52
|
+
export type BlockChangeSource =
|
|
53
|
+
| { type: "local" }
|
|
54
|
+
| { type: "paste" }
|
|
55
|
+
| { type: "drop" }
|
|
56
|
+
| { type: "undo" | "redo" | "undo-redo" }
|
|
57
|
+
| { type: "yjs-remote" };
|
|
58
|
+
|
|
59
|
+
export type BlocksChanged<
|
|
60
|
+
BSchema extends BlockSchema = DefaultBlockSchema,
|
|
61
|
+
ISchema extends InlineContentSchema = DefaultInlineContentSchema,
|
|
62
|
+
SSchema extends StyleSchema = DefaultStyleSchema,
|
|
63
|
+
> = Array<
|
|
64
|
+
{
|
|
65
|
+
/**
|
|
66
|
+
* The affected block.
|
|
67
|
+
*/
|
|
68
|
+
block: Block<BSchema, ISchema, SSchema>;
|
|
69
|
+
/**
|
|
70
|
+
* The source of the change.
|
|
71
|
+
*/
|
|
72
|
+
source: BlockChangeSource;
|
|
73
|
+
} & (
|
|
74
|
+
| {
|
|
75
|
+
type: "insert" | "delete";
|
|
76
|
+
/**
|
|
77
|
+
* Insert and delete changes don't have a previous block.
|
|
78
|
+
*/
|
|
79
|
+
prevBlock: undefined;
|
|
80
|
+
}
|
|
81
|
+
| {
|
|
82
|
+
type: "update";
|
|
83
|
+
/**
|
|
84
|
+
* The previous block.
|
|
85
|
+
*/
|
|
86
|
+
prevBlock: Block<BSchema, ISchema, SSchema>;
|
|
87
|
+
}
|
|
88
|
+
| {
|
|
89
|
+
type: "move";
|
|
90
|
+
/**
|
|
91
|
+
* The affected block.
|
|
92
|
+
*/
|
|
93
|
+
block: Block<BSchema, ISchema, SSchema>;
|
|
94
|
+
/**
|
|
95
|
+
* The block before the move.
|
|
96
|
+
*/
|
|
97
|
+
prevBlock: Block<BSchema, ISchema, SSchema>;
|
|
98
|
+
/**
|
|
99
|
+
* The previous parent block (if it existed).
|
|
100
|
+
*/
|
|
101
|
+
prevParent?: Block<BSchema, ISchema, SSchema>;
|
|
102
|
+
/**
|
|
103
|
+
* The current parent block (if it exists).
|
|
104
|
+
*/
|
|
105
|
+
currentParent?: Block<BSchema, ISchema, SSchema>;
|
|
106
|
+
}
|
|
107
|
+
)
|
|
108
|
+
>;
|
|
109
|
+
|
|
110
|
+
function determineChangeSource(transaction: Transaction): BlockChangeSource {
|
|
111
|
+
if (transaction.getMeta("paste")) {
|
|
112
|
+
return { type: "paste" };
|
|
113
|
+
}
|
|
114
|
+
if (transaction.getMeta("uiEvent") === "drop") {
|
|
115
|
+
return { type: "drop" };
|
|
116
|
+
}
|
|
117
|
+
if (transaction.getMeta("history$")) {
|
|
118
|
+
return {
|
|
119
|
+
type: transaction.getMeta("history$").redo ? "redo" : "undo",
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
if (transaction.getMeta("y-sync$")) {
|
|
123
|
+
if (transaction.getMeta("y-sync$").isUndoRedoOperation) {
|
|
124
|
+
return { type: "undo-redo" };
|
|
125
|
+
}
|
|
126
|
+
return { type: "yjs-remote" };
|
|
127
|
+
}
|
|
128
|
+
return { type: "local" };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
type BlockSnapshot<
|
|
132
|
+
BSchema extends BlockSchema,
|
|
133
|
+
ISchema extends InlineContentSchema,
|
|
134
|
+
SSchema extends StyleSchema,
|
|
135
|
+
> = {
|
|
136
|
+
byId: Record<
|
|
137
|
+
string,
|
|
138
|
+
{
|
|
139
|
+
block: Block<BSchema, ISchema, SSchema>;
|
|
140
|
+
parentId: string | undefined;
|
|
141
|
+
}
|
|
142
|
+
>;
|
|
143
|
+
childrenByParent: Record<string, string[]>;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Collects a snapshot of blocks and per-parent child order in a single traversal.
|
|
148
|
+
* Uses "__root__" to represent the root level where parentId is undefined.
|
|
149
|
+
*/
|
|
150
|
+
function collectSnapshot<
|
|
151
|
+
BSchema extends BlockSchema,
|
|
152
|
+
ISchema extends InlineContentSchema,
|
|
153
|
+
SSchema extends StyleSchema,
|
|
154
|
+
>(doc: Node): BlockSnapshot<BSchema, ISchema, SSchema> {
|
|
155
|
+
const ROOT_KEY = "__root__";
|
|
156
|
+
const byId: Record<
|
|
157
|
+
string,
|
|
158
|
+
{
|
|
159
|
+
block: Block<BSchema, ISchema, SSchema>;
|
|
160
|
+
parentId: string | undefined;
|
|
161
|
+
}
|
|
162
|
+
> = {};
|
|
163
|
+
const childrenByParent: Record<string, string[]> = {};
|
|
164
|
+
const pmSchema = getPmSchema(doc);
|
|
165
|
+
doc.descendants((node, pos) => {
|
|
166
|
+
if (!isNodeBlock(node)) {
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
const parentId = getParentBlockId(doc, pos);
|
|
170
|
+
const key = parentId ?? ROOT_KEY;
|
|
171
|
+
if (!childrenByParent[key]) {
|
|
172
|
+
childrenByParent[key] = [];
|
|
173
|
+
}
|
|
174
|
+
const block = nodeToBlock(node, pmSchema);
|
|
175
|
+
byId[node.attrs.id] = { block, parentId };
|
|
176
|
+
childrenByParent[key].push(node.attrs.id);
|
|
177
|
+
return true;
|
|
178
|
+
});
|
|
179
|
+
return { byId, childrenByParent };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Determines which child ids have been reordered (moved) within the same parent.
|
|
184
|
+
* Uses LIS to keep the longest ordered subsequence and marks the rest as moved.
|
|
185
|
+
*/
|
|
186
|
+
function detectReorderedChildren(
|
|
187
|
+
prevOrder: string[] | undefined,
|
|
188
|
+
nextOrder: string[] | undefined,
|
|
189
|
+
): Set<string> {
|
|
190
|
+
const moved = new Set<string>();
|
|
191
|
+
if (!prevOrder || !nextOrder) {
|
|
192
|
+
return moved;
|
|
193
|
+
}
|
|
194
|
+
// Consider only ids present in both orders (ignore inserts/deletes handled elsewhere)
|
|
195
|
+
const prevIds = new Set(prevOrder);
|
|
196
|
+
const commonNext: string[] = nextOrder.filter((id) => prevIds.has(id));
|
|
197
|
+
const commonPrev: string[] = prevOrder.filter((id) =>
|
|
198
|
+
commonNext.includes(id),
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
if (commonPrev.length <= 1 || commonNext.length <= 1) {
|
|
202
|
+
return moved;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Map ids to their index in previous order
|
|
206
|
+
const indexInPrev: Record<string, number> = {};
|
|
207
|
+
for (let i = 0; i < commonPrev.length; i++) {
|
|
208
|
+
indexInPrev[commonPrev[i]] = i;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Build sequence of indices representing next order in terms of previous indices
|
|
212
|
+
const sequence: number[] = commonNext.map((id) => indexInPrev[id]);
|
|
213
|
+
|
|
214
|
+
// Inline O(n log n) LIS with reconstruction.
|
|
215
|
+
// Why LIS? We want the smallest set of siblings to label as "moved".
|
|
216
|
+
// Keeping the longest subsequence that is already in order achieves this,
|
|
217
|
+
// so only items outside the LIS are reported as moves.
|
|
218
|
+
const n = sequence.length;
|
|
219
|
+
const tailsValues: number[] = [];
|
|
220
|
+
const tailsEndsAtIndex: number[] = [];
|
|
221
|
+
const previousIndexInLis: number[] = new Array(n).fill(-1);
|
|
222
|
+
|
|
223
|
+
const lowerBound = (arr: number[], target: number): number => {
|
|
224
|
+
let lo = 0;
|
|
225
|
+
let hi = arr.length;
|
|
226
|
+
while (lo < hi) {
|
|
227
|
+
const mid = (lo + hi) >>> 1;
|
|
228
|
+
if (arr[mid] < target) {
|
|
229
|
+
lo = mid + 1;
|
|
230
|
+
} else {
|
|
231
|
+
hi = mid;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return lo;
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
for (let i = 0; i < n; i++) {
|
|
238
|
+
const value = sequence[i];
|
|
239
|
+
const pos = lowerBound(tailsValues, value);
|
|
240
|
+
if (pos > 0) {
|
|
241
|
+
previousIndexInLis[i] = tailsEndsAtIndex[pos - 1];
|
|
242
|
+
}
|
|
243
|
+
if (pos === tailsValues.length) {
|
|
244
|
+
tailsValues.push(value);
|
|
245
|
+
tailsEndsAtIndex.push(i);
|
|
246
|
+
} else {
|
|
247
|
+
tailsValues[pos] = value;
|
|
248
|
+
tailsEndsAtIndex[pos] = i;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const lisIndexSet = new Set<number>();
|
|
253
|
+
let k = tailsEndsAtIndex[tailsEndsAtIndex.length - 1] ?? -1;
|
|
254
|
+
while (k !== -1) {
|
|
255
|
+
lisIndexSet.add(k);
|
|
256
|
+
k = previousIndexInLis[k];
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Items not part of LIS are considered moved
|
|
260
|
+
for (let i = 0; i < commonNext.length; i++) {
|
|
261
|
+
if (!lisIndexSet.has(i)) {
|
|
262
|
+
moved.add(commonNext[i]);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return moved;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Get the blocks that were changed by a transaction.
|
|
270
|
+
*/
|
|
271
|
+
export function getBlocksChangedByTransaction<
|
|
272
|
+
BSchema extends BlockSchema = DefaultBlockSchema,
|
|
273
|
+
ISchema extends InlineContentSchema = DefaultInlineContentSchema,
|
|
274
|
+
SSchema extends StyleSchema = DefaultStyleSchema,
|
|
275
|
+
>(
|
|
276
|
+
transaction: Transaction,
|
|
277
|
+
appendedTransactions: Transaction[] = [],
|
|
278
|
+
): BlocksChanged<BSchema, ISchema, SSchema> {
|
|
279
|
+
const source = determineChangeSource(transaction);
|
|
280
|
+
const combinedTransaction = combineTransactionSteps(transaction.before, [
|
|
281
|
+
transaction,
|
|
282
|
+
...appendedTransactions,
|
|
283
|
+
]);
|
|
284
|
+
|
|
285
|
+
const prevSnap = collectSnapshot<BSchema, ISchema, SSchema>(
|
|
286
|
+
combinedTransaction.before,
|
|
287
|
+
);
|
|
288
|
+
const nextSnap = collectSnapshot<BSchema, ISchema, SSchema>(
|
|
289
|
+
combinedTransaction.doc,
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
const changes: BlocksChanged<BSchema, ISchema, SSchema> = [];
|
|
293
|
+
const changedIds = new Set<string>();
|
|
294
|
+
|
|
295
|
+
// Handle inserted blocks
|
|
296
|
+
Object.keys(nextSnap.byId)
|
|
297
|
+
.filter((id) => !(id in prevSnap.byId))
|
|
298
|
+
.forEach((id) => {
|
|
299
|
+
changes.push({
|
|
300
|
+
type: "insert",
|
|
301
|
+
block: nextSnap.byId[id].block,
|
|
302
|
+
source,
|
|
303
|
+
prevBlock: undefined,
|
|
304
|
+
});
|
|
305
|
+
changedIds.add(id);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// Handle deleted blocks
|
|
309
|
+
Object.keys(prevSnap.byId)
|
|
310
|
+
.filter((id) => !(id in nextSnap.byId))
|
|
311
|
+
.forEach((id) => {
|
|
312
|
+
changes.push({
|
|
313
|
+
type: "delete",
|
|
314
|
+
block: prevSnap.byId[id].block,
|
|
315
|
+
source,
|
|
316
|
+
prevBlock: undefined,
|
|
317
|
+
});
|
|
318
|
+
changedIds.add(id);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// Handle updated, moved to different parent, indented, outdented blocks
|
|
322
|
+
Object.keys(nextSnap.byId)
|
|
323
|
+
.filter((id) => id in prevSnap.byId)
|
|
324
|
+
.forEach((id) => {
|
|
325
|
+
const prev = prevSnap.byId[id];
|
|
326
|
+
const next = nextSnap.byId[id];
|
|
327
|
+
const isParentDifferent = prev.parentId !== next.parentId;
|
|
328
|
+
|
|
329
|
+
if (isParentDifferent) {
|
|
330
|
+
changes.push({
|
|
331
|
+
type: "move",
|
|
332
|
+
block: next.block,
|
|
333
|
+
prevBlock: prev.block,
|
|
334
|
+
source,
|
|
335
|
+
prevParent: prev.parentId
|
|
336
|
+
? prevSnap.byId[prev.parentId]?.block
|
|
337
|
+
: undefined,
|
|
338
|
+
currentParent: next.parentId
|
|
339
|
+
? nextSnap.byId[next.parentId]?.block
|
|
340
|
+
: undefined,
|
|
341
|
+
});
|
|
342
|
+
changedIds.add(id);
|
|
343
|
+
} else if (
|
|
344
|
+
// Compare blocks while ignoring children to avoid reporting a parent
|
|
345
|
+
// update when only descendants changed.
|
|
346
|
+
!deepEqual(
|
|
347
|
+
{ ...prev.block, children: undefined } as any,
|
|
348
|
+
{ ...next.block, children: undefined } as any,
|
|
349
|
+
)
|
|
350
|
+
) {
|
|
351
|
+
changes.push({
|
|
352
|
+
type: "update",
|
|
353
|
+
block: next.block,
|
|
354
|
+
prevBlock: prev.block,
|
|
355
|
+
source,
|
|
356
|
+
});
|
|
357
|
+
changedIds.add(id);
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// Handle sibling reorders (parent unchanged but relative order changed)
|
|
362
|
+
const prevOrderByParent = prevSnap.childrenByParent;
|
|
363
|
+
const nextOrderByParent = nextSnap.childrenByParent;
|
|
364
|
+
|
|
365
|
+
// Use a special key for root-level siblings
|
|
366
|
+
const ROOT_KEY = "__root__";
|
|
367
|
+
const parents = new Set<string>([
|
|
368
|
+
...Object.keys(prevOrderByParent),
|
|
369
|
+
...Object.keys(nextOrderByParent),
|
|
370
|
+
]);
|
|
371
|
+
|
|
372
|
+
const addedMoveForId = new Set<string>();
|
|
373
|
+
|
|
374
|
+
parents.forEach((parentKey) => {
|
|
375
|
+
const movedWithinParent = detectReorderedChildren(
|
|
376
|
+
prevOrderByParent[parentKey],
|
|
377
|
+
nextOrderByParent[parentKey],
|
|
378
|
+
);
|
|
379
|
+
if (movedWithinParent.size === 0) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
movedWithinParent.forEach((id) => {
|
|
383
|
+
// Only consider ids that exist in both snapshots and whose parent truly did not change
|
|
384
|
+
const prev = prevSnap.byId[id];
|
|
385
|
+
const next = nextSnap.byId[id];
|
|
386
|
+
if (!prev || !next) {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
if (prev.parentId !== next.parentId) {
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
// Skip if already accounted for by insert/delete/update/parent move
|
|
393
|
+
if (changedIds.has(id)) {
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
// Verify we're addressing the right parent bucket
|
|
397
|
+
const bucketKey = prev.parentId ?? ROOT_KEY;
|
|
398
|
+
if (bucketKey !== parentKey) {
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
if (addedMoveForId.has(id)) {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
addedMoveForId.add(id);
|
|
405
|
+
changes.push({
|
|
406
|
+
type: "move",
|
|
407
|
+
block: next.block,
|
|
408
|
+
prevBlock: prev.block,
|
|
409
|
+
source,
|
|
410
|
+
prevParent: prev.parentId
|
|
411
|
+
? prevSnap.byId[prev.parentId]?.block
|
|
412
|
+
: undefined,
|
|
413
|
+
currentParent: next.parentId
|
|
414
|
+
? nextSnap.byId[next.parentId]?.block
|
|
415
|
+
: undefined,
|
|
416
|
+
});
|
|
417
|
+
changedIds.add(id);
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
return changes;
|
|
422
|
+
}
|