@blocknote/core 0.36.0 → 0.36.1
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 +1141 -1078
- package/dist/blocknote.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +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/TableBlockContent/TableBlockContent.ts +31 -4
- package/src/editor/BlockNoteEditor.ts +1 -1
- 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/types/src/api/{nodeUtil.test.d.ts → getBlocksChangedByTransaction.test.d.ts} +0 -0
|
@@ -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
|
+
}
|
package/src/api/nodeUtil.ts
CHANGED
|
@@ -1,34 +1,4 @@
|
|
|
1
|
-
import { combineTransactionSteps } from "@tiptap/core";
|
|
2
1
|
import type { Node } from "prosemirror-model";
|
|
3
|
-
import type { Transaction } from "prosemirror-state";
|
|
4
|
-
import {
|
|
5
|
-
Block,
|
|
6
|
-
DefaultBlockSchema,
|
|
7
|
-
DefaultInlineContentSchema,
|
|
8
|
-
DefaultStyleSchema,
|
|
9
|
-
} from "../blocks/defaultBlocks.js";
|
|
10
|
-
import type { BlockSchema } from "../schema/index.js";
|
|
11
|
-
import type { InlineContentSchema } from "../schema/inlineContent/types.js";
|
|
12
|
-
import type { StyleSchema } from "../schema/styles/types.js";
|
|
13
|
-
import { nodeToBlock } from "./nodeConversions/nodeToBlock.js";
|
|
14
|
-
import { getPmSchema } from "./pmUtil.js";
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Gets the parent block of a node, if it has one.
|
|
18
|
-
*/
|
|
19
|
-
function getParentBlockId(doc: Node, pos: number): string | undefined {
|
|
20
|
-
if (pos === 0) {
|
|
21
|
-
return undefined;
|
|
22
|
-
}
|
|
23
|
-
const resolvedPos = doc.resolve(pos);
|
|
24
|
-
for (let i = resolvedPos.depth; i > 0; i--) {
|
|
25
|
-
const parent = resolvedPos.node(i);
|
|
26
|
-
if (isNodeBlock(parent)) {
|
|
27
|
-
return parent.attrs.id;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
return undefined;
|
|
31
|
-
}
|
|
32
2
|
|
|
33
3
|
/**
|
|
34
4
|
* Get a TipTap node by id
|
|
@@ -70,223 +40,3 @@ export function getNodeById(
|
|
|
70
40
|
export function isNodeBlock(node: Node): boolean {
|
|
71
41
|
return node.type.isInGroup("bnBlock");
|
|
72
42
|
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* This attributes the changes to a specific source.
|
|
76
|
-
*/
|
|
77
|
-
export type BlockChangeSource =
|
|
78
|
-
| { type: "local" }
|
|
79
|
-
| { type: "paste" }
|
|
80
|
-
| { type: "drop" }
|
|
81
|
-
| { type: "undo" | "redo" | "undo-redo" }
|
|
82
|
-
| { type: "yjs-remote" };
|
|
83
|
-
|
|
84
|
-
export type BlocksChanged<
|
|
85
|
-
BSchema extends BlockSchema = DefaultBlockSchema,
|
|
86
|
-
ISchema extends InlineContentSchema = DefaultInlineContentSchema,
|
|
87
|
-
SSchema extends StyleSchema = DefaultStyleSchema,
|
|
88
|
-
> = Array<
|
|
89
|
-
{
|
|
90
|
-
/**
|
|
91
|
-
* The affected block.
|
|
92
|
-
*/
|
|
93
|
-
block: Block<BSchema, ISchema, SSchema>;
|
|
94
|
-
/**
|
|
95
|
-
* The source of the change.
|
|
96
|
-
*/
|
|
97
|
-
source: BlockChangeSource;
|
|
98
|
-
} & (
|
|
99
|
-
| {
|
|
100
|
-
type: "insert" | "delete";
|
|
101
|
-
/**
|
|
102
|
-
* Insert and delete changes don't have a previous block.
|
|
103
|
-
*/
|
|
104
|
-
prevBlock: undefined;
|
|
105
|
-
}
|
|
106
|
-
| {
|
|
107
|
-
type: "update";
|
|
108
|
-
/**
|
|
109
|
-
* The previous block.
|
|
110
|
-
*/
|
|
111
|
-
prevBlock: Block<BSchema, ISchema, SSchema>;
|
|
112
|
-
}
|
|
113
|
-
| {
|
|
114
|
-
type: "move";
|
|
115
|
-
/**
|
|
116
|
-
* The affected block.
|
|
117
|
-
*/
|
|
118
|
-
block: Block<BSchema, ISchema, SSchema>;
|
|
119
|
-
/**
|
|
120
|
-
* The block before the move.
|
|
121
|
-
*/
|
|
122
|
-
prevBlock: Block<BSchema, ISchema, SSchema>;
|
|
123
|
-
/**
|
|
124
|
-
* The previous parent block (if it existed).
|
|
125
|
-
*/
|
|
126
|
-
prevParent?: Block<BSchema, ISchema, SSchema>;
|
|
127
|
-
/**
|
|
128
|
-
* The current parent block (if it exists).
|
|
129
|
-
*/
|
|
130
|
-
currentParent?: Block<BSchema, ISchema, SSchema>;
|
|
131
|
-
}
|
|
132
|
-
)
|
|
133
|
-
>;
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Compares two blocks, ignoring their children.
|
|
137
|
-
* Returns true if the blocks are different (excluding children).
|
|
138
|
-
*/
|
|
139
|
-
function areBlocksDifferentExcludingChildren<
|
|
140
|
-
BSchema extends BlockSchema,
|
|
141
|
-
ISchema extends InlineContentSchema,
|
|
142
|
-
SSchema extends StyleSchema,
|
|
143
|
-
>(
|
|
144
|
-
block1: Block<BSchema, ISchema, SSchema>,
|
|
145
|
-
block2: Block<BSchema, ISchema, SSchema>,
|
|
146
|
-
): boolean {
|
|
147
|
-
return (
|
|
148
|
-
block1.id !== block2.id ||
|
|
149
|
-
block1.type !== block2.type ||
|
|
150
|
-
JSON.stringify(block1.props) !== JSON.stringify(block2.props) ||
|
|
151
|
-
JSON.stringify(block1.content) !== JSON.stringify(block2.content)
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function determineChangeSource(transaction: Transaction): BlockChangeSource {
|
|
156
|
-
if (transaction.getMeta("paste")) {
|
|
157
|
-
return { type: "paste" };
|
|
158
|
-
}
|
|
159
|
-
if (transaction.getMeta("uiEvent") === "drop") {
|
|
160
|
-
return { type: "drop" };
|
|
161
|
-
}
|
|
162
|
-
if (transaction.getMeta("history$")) {
|
|
163
|
-
return {
|
|
164
|
-
type: transaction.getMeta("history$").redo ? "redo" : "undo",
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
if (transaction.getMeta("y-sync$")) {
|
|
168
|
-
if (transaction.getMeta("y-sync$").isUndoRedoOperation) {
|
|
169
|
-
return { type: "undo-redo" };
|
|
170
|
-
}
|
|
171
|
-
return { type: "yjs-remote" };
|
|
172
|
-
}
|
|
173
|
-
return { type: "local" };
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function collectAllBlocks<
|
|
177
|
-
BSchema extends BlockSchema,
|
|
178
|
-
ISchema extends InlineContentSchema,
|
|
179
|
-
SSchema extends StyleSchema,
|
|
180
|
-
>(
|
|
181
|
-
doc: Node,
|
|
182
|
-
): Record<
|
|
183
|
-
string,
|
|
184
|
-
{
|
|
185
|
-
block: Block<BSchema, ISchema, SSchema>;
|
|
186
|
-
parentId: string | undefined;
|
|
187
|
-
}
|
|
188
|
-
> {
|
|
189
|
-
const blocks: Record<
|
|
190
|
-
string,
|
|
191
|
-
{
|
|
192
|
-
block: Block<BSchema, ISchema, SSchema>;
|
|
193
|
-
parentId: string | undefined;
|
|
194
|
-
}
|
|
195
|
-
> = {};
|
|
196
|
-
const pmSchema = getPmSchema(doc);
|
|
197
|
-
doc.descendants((node, pos) => {
|
|
198
|
-
if (isNodeBlock(node)) {
|
|
199
|
-
const parentId = getParentBlockId(doc, pos);
|
|
200
|
-
blocks[node.attrs.id] = {
|
|
201
|
-
block: nodeToBlock(node, pmSchema),
|
|
202
|
-
parentId,
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
return true;
|
|
206
|
-
});
|
|
207
|
-
return blocks;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Get the blocks that were changed by a transaction.
|
|
212
|
-
*/
|
|
213
|
-
export function getBlocksChangedByTransaction<
|
|
214
|
-
BSchema extends BlockSchema = DefaultBlockSchema,
|
|
215
|
-
ISchema extends InlineContentSchema = DefaultInlineContentSchema,
|
|
216
|
-
SSchema extends StyleSchema = DefaultStyleSchema,
|
|
217
|
-
>(
|
|
218
|
-
transaction: Transaction,
|
|
219
|
-
appendedTransactions: Transaction[] = [],
|
|
220
|
-
): BlocksChanged<BSchema, ISchema, SSchema> {
|
|
221
|
-
const source = determineChangeSource(transaction);
|
|
222
|
-
const combinedTransaction = combineTransactionSteps(transaction.before, [
|
|
223
|
-
transaction,
|
|
224
|
-
...appendedTransactions,
|
|
225
|
-
]);
|
|
226
|
-
|
|
227
|
-
const prevBlocks = collectAllBlocks<BSchema, ISchema, SSchema>(
|
|
228
|
-
combinedTransaction.before,
|
|
229
|
-
);
|
|
230
|
-
const nextBlocks = collectAllBlocks<BSchema, ISchema, SSchema>(
|
|
231
|
-
combinedTransaction.doc,
|
|
232
|
-
);
|
|
233
|
-
|
|
234
|
-
const changes: BlocksChanged<BSchema, ISchema, SSchema> = [];
|
|
235
|
-
|
|
236
|
-
// Handle inserted blocks
|
|
237
|
-
Object.keys(nextBlocks)
|
|
238
|
-
.filter((id) => !(id in prevBlocks))
|
|
239
|
-
.forEach((id) => {
|
|
240
|
-
changes.push({
|
|
241
|
-
type: "insert",
|
|
242
|
-
block: nextBlocks[id].block,
|
|
243
|
-
source,
|
|
244
|
-
prevBlock: undefined,
|
|
245
|
-
});
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
// Handle deleted blocks
|
|
249
|
-
Object.keys(prevBlocks)
|
|
250
|
-
.filter((id) => !(id in nextBlocks))
|
|
251
|
-
.forEach((id) => {
|
|
252
|
-
changes.push({
|
|
253
|
-
type: "delete",
|
|
254
|
-
block: prevBlocks[id].block,
|
|
255
|
-
source,
|
|
256
|
-
prevBlock: undefined,
|
|
257
|
-
});
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
// Handle updated, moved, indented, outdented blocks
|
|
261
|
-
Object.keys(nextBlocks)
|
|
262
|
-
.filter((id) => id in prevBlocks)
|
|
263
|
-
.forEach((id) => {
|
|
264
|
-
const prev = prevBlocks[id];
|
|
265
|
-
const next = nextBlocks[id];
|
|
266
|
-
const isParentDifferent = prev.parentId !== next.parentId;
|
|
267
|
-
|
|
268
|
-
if (isParentDifferent) {
|
|
269
|
-
changes.push({
|
|
270
|
-
type: "move",
|
|
271
|
-
block: next.block,
|
|
272
|
-
prevBlock: prev.block,
|
|
273
|
-
source,
|
|
274
|
-
prevParent: prev.parentId
|
|
275
|
-
? prevBlocks[prev.parentId]?.block
|
|
276
|
-
: undefined,
|
|
277
|
-
currentParent: next.parentId
|
|
278
|
-
? nextBlocks[next.parentId]?.block
|
|
279
|
-
: undefined,
|
|
280
|
-
});
|
|
281
|
-
} else if (areBlocksDifferentExcludingChildren(prev.block, next.block)) {
|
|
282
|
-
changes.push({
|
|
283
|
-
type: "update",
|
|
284
|
-
block: next.block,
|
|
285
|
-
prevBlock: prev.block,
|
|
286
|
-
source,
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
return changes;
|
|
292
|
-
}
|