@verdant-web/tiptap 1.0.1 → 2.1.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/esm/__browserTests__/react.test.js +152 -13
- package/dist/esm/__browserTests__/react.test.js.map +1 -1
- package/dist/esm/extensions/NodeId.d.ts +2 -0
- package/dist/esm/extensions/NodeId.js +66 -0
- package/dist/esm/extensions/NodeId.js.map +1 -0
- package/dist/esm/{plugins.d.ts → extensions/Verdant.d.ts} +0 -1
- package/dist/esm/{plugins.js → extensions/Verdant.js} +24 -81
- package/dist/esm/extensions/Verdant.js.map +1 -0
- package/dist/esm/extensions/VerdantMedia.d.ts +30 -0
- package/dist/esm/extensions/VerdantMedia.js +261 -0
- package/dist/esm/extensions/VerdantMedia.js.map +1 -0
- package/dist/esm/fields.d.ts +45 -10
- package/dist/esm/fields.js +18 -1
- package/dist/esm/fields.js.map +1 -1
- package/dist/esm/index.d.ts +3 -1
- package/dist/esm/index.js +3 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/react.d.ts +4 -2
- package/dist/esm/react.js +12 -4
- package/dist/esm/react.js.map +1 -1
- package/package.json +9 -8
- package/src/__browserTests__/fixtures/cat.jpg +0 -0
- package/src/__browserTests__/fixtures/cat.m4a +0 -0
- package/src/__browserTests__/fixtures/cat.mp4 +0 -0
- package/src/__browserTests__/react.test.tsx +227 -14
- package/src/extensions/NodeId.ts +67 -0
- package/src/{plugins.ts → extensions/Verdant.ts} +22 -85
- package/src/extensions/VerdantMedia.ts +299 -0
- package/src/fields.ts +61 -10
- package/src/index.ts +3 -1
- package/src/react.ts +22 -9
- package/dist/esm/plugins.js.map +0 -1
|
@@ -4,10 +4,13 @@ import { createMigration, schema } from '@verdant-web/common';
|
|
|
4
4
|
import { createHooks } from '@verdant-web/react';
|
|
5
5
|
import { ClientDescriptor, ClientWithCollections } from '@verdant-web/store';
|
|
6
6
|
import { userEvent } from '@vitest/browser/context';
|
|
7
|
-
import { ReactNode, Suspense } from 'react';
|
|
7
|
+
import { ChangeEvent, ReactNode, Suspense } from 'react';
|
|
8
8
|
import { afterAll, beforeAll, expect, it } from 'vitest';
|
|
9
9
|
import { render } from 'vitest-browser-react';
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
createTipTapFieldSchema,
|
|
12
|
+
createTipTapFileMapSchema,
|
|
13
|
+
} from '../fields.js';
|
|
11
14
|
import { useSyncedEditor } from '../react.js';
|
|
12
15
|
|
|
13
16
|
const testSchema = schema({
|
|
@@ -25,6 +28,7 @@ const testSchema = schema({
|
|
|
25
28
|
content: [],
|
|
26
29
|
},
|
|
27
30
|
}),
|
|
31
|
+
files: createTipTapFileMapSchema(),
|
|
28
32
|
},
|
|
29
33
|
}),
|
|
30
34
|
},
|
|
@@ -32,6 +36,19 @@ const testSchema = schema({
|
|
|
32
36
|
|
|
33
37
|
const hooks = createHooks(testSchema);
|
|
34
38
|
|
|
39
|
+
function safeLogger(level: string, ...args: any[]) {
|
|
40
|
+
try {
|
|
41
|
+
if (args.some((arg) => typeof arg === 'string' && arg.includes('Redo')))
|
|
42
|
+
console.debug(
|
|
43
|
+
...args.map((arg) =>
|
|
44
|
+
typeof arg === 'object' ? JSON.stringify(arg, undefined, ' ') : arg,
|
|
45
|
+
),
|
|
46
|
+
);
|
|
47
|
+
} catch (err) {
|
|
48
|
+
console.debug(...args);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
35
52
|
let clientDesc: ClientDescriptor;
|
|
36
53
|
let client: ClientWithCollections;
|
|
37
54
|
beforeAll(async () => {
|
|
@@ -40,6 +57,7 @@ beforeAll(async () => {
|
|
|
40
57
|
oldSchemas: [testSchema],
|
|
41
58
|
migrations: [createMigration(testSchema)],
|
|
42
59
|
namespace: 'tiptatp-test',
|
|
60
|
+
// log: safeLogger,
|
|
43
61
|
});
|
|
44
62
|
client = await (clientDesc.open() as Promise<ClientWithCollections>);
|
|
45
63
|
});
|
|
@@ -107,20 +125,20 @@ it('should support non-nullable tiptap schema fields', async () => {
|
|
|
107
125
|
{
|
|
108
126
|
type: 'text',
|
|
109
127
|
attrs: {},
|
|
110
|
-
content:
|
|
111
|
-
marks:
|
|
128
|
+
content: null,
|
|
129
|
+
marks: null,
|
|
112
130
|
from: null,
|
|
113
131
|
to: null,
|
|
114
132
|
text: 'Hello, world!',
|
|
115
133
|
},
|
|
116
134
|
],
|
|
117
|
-
marks:
|
|
135
|
+
marks: null,
|
|
118
136
|
from: null,
|
|
119
137
|
to: null,
|
|
120
138
|
text: null,
|
|
121
139
|
},
|
|
122
140
|
],
|
|
123
|
-
marks:
|
|
141
|
+
marks: null,
|
|
124
142
|
text: null,
|
|
125
143
|
});
|
|
126
144
|
});
|
|
@@ -176,20 +194,20 @@ it('should support nullable tiptap schema fields with a specified default doc',
|
|
|
176
194
|
{
|
|
177
195
|
type: 'text',
|
|
178
196
|
attrs: {},
|
|
179
|
-
content:
|
|
180
|
-
marks:
|
|
197
|
+
content: null,
|
|
198
|
+
marks: null,
|
|
181
199
|
from: null,
|
|
182
200
|
to: null,
|
|
183
201
|
text: 'Hello, world!',
|
|
184
202
|
},
|
|
185
203
|
],
|
|
186
|
-
marks:
|
|
204
|
+
marks: null,
|
|
187
205
|
from: null,
|
|
188
206
|
to: null,
|
|
189
207
|
text: null,
|
|
190
208
|
},
|
|
191
209
|
],
|
|
192
|
-
marks:
|
|
210
|
+
marks: null,
|
|
193
211
|
text: null,
|
|
194
212
|
});
|
|
195
213
|
});
|
|
@@ -262,7 +280,7 @@ it('should support Verdant undo and redo', async () => {
|
|
|
262
280
|
from: null,
|
|
263
281
|
to: null,
|
|
264
282
|
content: [],
|
|
265
|
-
marks:
|
|
283
|
+
marks: null,
|
|
266
284
|
text: null,
|
|
267
285
|
});
|
|
268
286
|
});
|
|
@@ -333,15 +351,15 @@ it('should support TipTap undo and redo', async () => {
|
|
|
333
351
|
content: [
|
|
334
352
|
{
|
|
335
353
|
attrs: {},
|
|
336
|
-
content:
|
|
354
|
+
content: null,
|
|
337
355
|
from: null,
|
|
338
|
-
marks:
|
|
356
|
+
marks: null,
|
|
339
357
|
text: null,
|
|
340
358
|
to: null,
|
|
341
359
|
type: 'paragraph',
|
|
342
360
|
},
|
|
343
361
|
],
|
|
344
|
-
marks:
|
|
362
|
+
marks: null,
|
|
345
363
|
text: null,
|
|
346
364
|
});
|
|
347
365
|
});
|
|
@@ -394,7 +412,202 @@ it('should initialize from an existing document', async () => {
|
|
|
394
412
|
const screen = await renderWithProvider(<TipTapTest />);
|
|
395
413
|
await expect.element(screen.getByTestId('editor')).toBeVisible();
|
|
396
414
|
|
|
415
|
+
const editor = screen.getByTestId('editor').getByRole('textbox');
|
|
416
|
+
await expect.element(editor).toHaveTextContent('Hello, world!');
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
function testLog(...args: any[]) {
|
|
420
|
+
console.debug('[TEST]', ...args);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
it('should withstand multiple edits, undo, and redo consistently', async () => {
|
|
424
|
+
const testPost = await client.posts.put({
|
|
425
|
+
requiredBody: {
|
|
426
|
+
type: 'doc',
|
|
427
|
+
content: [
|
|
428
|
+
{
|
|
429
|
+
type: 'paragraph',
|
|
430
|
+
content: [
|
|
431
|
+
{
|
|
432
|
+
type: 'text',
|
|
433
|
+
text: 'Hello, world!',
|
|
434
|
+
},
|
|
435
|
+
],
|
|
436
|
+
},
|
|
437
|
+
],
|
|
438
|
+
},
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
// reset history
|
|
442
|
+
await client.entities.flushAllBatches();
|
|
443
|
+
client.undoHistory.clear();
|
|
444
|
+
|
|
445
|
+
const TipTapTest = () => {
|
|
446
|
+
const editor = useSyncedEditor(testPost, 'requiredBody', {
|
|
447
|
+
editorOptions: {
|
|
448
|
+
extensions: [StarterKit.configure({ history: false })],
|
|
449
|
+
},
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
return (
|
|
453
|
+
<div>
|
|
454
|
+
<div>Text editor:</div>
|
|
455
|
+
<EditorContent
|
|
456
|
+
style={{
|
|
457
|
+
width: 500,
|
|
458
|
+
height: 300,
|
|
459
|
+
}}
|
|
460
|
+
editor={editor}
|
|
461
|
+
id="#editor"
|
|
462
|
+
data-testid="editor"
|
|
463
|
+
/>
|
|
464
|
+
</div>
|
|
465
|
+
);
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
const screen = await renderWithProvider(<TipTapTest />);
|
|
469
|
+
await expect.element(screen.getByTestId('editor')).toBeVisible();
|
|
470
|
+
|
|
471
|
+
const editor = screen.getByTestId('editor').getByRole('textbox');
|
|
472
|
+
await expect.element(editor).toHaveTextContent('Hello, world!');
|
|
473
|
+
|
|
474
|
+
await userEvent.type(editor, '[pageDown]');
|
|
475
|
+
await userEvent.type(editor, '\nLine 2!');
|
|
476
|
+
|
|
477
|
+
await client.entities.flushAllBatches();
|
|
478
|
+
|
|
479
|
+
await userEvent.type(editor, '\nLine 3!');
|
|
480
|
+
|
|
481
|
+
await client.entities.flushAllBatches();
|
|
482
|
+
|
|
483
|
+
expect(client.undoHistory.canUndo).toBe(true);
|
|
484
|
+
|
|
485
|
+
await client.undoHistory.undo();
|
|
486
|
+
await client.entities.flushAllBatches();
|
|
487
|
+
|
|
488
|
+
await expect.element(editor).toHaveTextContent(/^Hello, world!Line 2!$/);
|
|
489
|
+
|
|
490
|
+
await client.undoHistory.redo();
|
|
491
|
+
await client.entities.flushAllBatches();
|
|
492
|
+
|
|
493
|
+
await expect
|
|
494
|
+
.element(editor)
|
|
495
|
+
.toHaveTextContent(/^Hello, world!Line 2!Line 3!$/);
|
|
496
|
+
|
|
497
|
+
await client.undoHistory.undo();
|
|
498
|
+
await client.entities.flushAllBatches();
|
|
499
|
+
|
|
500
|
+
await expect.element(editor).toHaveTextContent(/^Hello, world!Line 2!$/);
|
|
501
|
+
|
|
502
|
+
// I've noticed some strangeness when restoring a deleted block
|
|
503
|
+
// when the editor is not on the same line as the deletion,
|
|
504
|
+
// not sure if this is actually related to cursor position but
|
|
505
|
+
// testing anyway.
|
|
506
|
+
await userEvent.type(editor, '[arrowUp]');
|
|
507
|
+
|
|
508
|
+
await client.undoHistory.redo();
|
|
509
|
+
await client.entities.flushAllBatches();
|
|
510
|
+
|
|
511
|
+
await expect
|
|
512
|
+
.element(editor)
|
|
513
|
+
.toHaveTextContent(/^Hello, world!Line 2!Line 3!$/);
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
it('should support media nodes', async () => {
|
|
517
|
+
const testPost = await client.posts.put({});
|
|
518
|
+
|
|
519
|
+
// reset history
|
|
520
|
+
await client.entities.flushAllBatches();
|
|
521
|
+
client.undoHistory.clear();
|
|
522
|
+
|
|
523
|
+
const user = userEvent.setup();
|
|
524
|
+
|
|
525
|
+
const TipTapTest = () => {
|
|
526
|
+
const editor = useSyncedEditor(testPost, 'requiredBody', {
|
|
527
|
+
editorOptions: {
|
|
528
|
+
extensions: [StarterKit.configure({ history: false })],
|
|
529
|
+
},
|
|
530
|
+
files: testPost.get('files'),
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
const insertMedia = async (ev: ChangeEvent<HTMLInputElement>) => {
|
|
534
|
+
const file = ev.target.files?.[0];
|
|
535
|
+
if (!file) return;
|
|
536
|
+
editor?.chain().insertMedia(file).run();
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
return (
|
|
540
|
+
<div>
|
|
541
|
+
<input type="file" onChange={insertMedia} data-testid="insert-media" />
|
|
542
|
+
<div>Text editor:</div>
|
|
543
|
+
<EditorContent
|
|
544
|
+
style={{
|
|
545
|
+
width: 500,
|
|
546
|
+
height: 300,
|
|
547
|
+
}}
|
|
548
|
+
editor={editor}
|
|
549
|
+
id="#editor"
|
|
550
|
+
data-testid="editor"
|
|
551
|
+
/>
|
|
552
|
+
</div>
|
|
553
|
+
);
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
// load the image fixture to a file
|
|
557
|
+
const imageRes = await fetch('/cat.jpg', { method: 'GET' });
|
|
558
|
+
const imageBlob = await imageRes.blob();
|
|
559
|
+
|
|
560
|
+
const screen = await renderWithProvider(<TipTapTest />);
|
|
561
|
+
await expect.element(screen.getByTestId('editor')).toBeVisible();
|
|
562
|
+
|
|
397
563
|
const editor = screen.getByTestId('editor').getByRole('textbox');
|
|
398
564
|
await expect.element(editor).toHaveTextContent('');
|
|
565
|
+
|
|
566
|
+
// send keystrokes to the editor
|
|
567
|
+
await user.type(editor, 'Hello, world!');
|
|
399
568
|
await expect.element(editor).toHaveTextContent('Hello, world!');
|
|
569
|
+
|
|
570
|
+
// simulate paste of an image
|
|
571
|
+
// load the image into the clipboard
|
|
572
|
+
await user.upload(
|
|
573
|
+
screen.getByTestId('insert-media'),
|
|
574
|
+
new File([imageBlob], 'cat.jpg', { type: 'image/jpeg' }),
|
|
575
|
+
);
|
|
576
|
+
|
|
577
|
+
// the image should be visible in the editor
|
|
578
|
+
await expect.element(screen.getByRole('img')).toBeVisible();
|
|
579
|
+
|
|
580
|
+
const audioRes = await fetch('/cat.m4a', { method: 'GET' });
|
|
581
|
+
const audioBlob = await audioRes.blob();
|
|
582
|
+
await user.upload(
|
|
583
|
+
screen.getByTestId('insert-media'),
|
|
584
|
+
new File([audioBlob], 'cat.m4a', { type: 'audio/m4a' }),
|
|
585
|
+
);
|
|
586
|
+
|
|
587
|
+
// the audio should be visible in the editor
|
|
588
|
+
const audio = screen.container.querySelector('audio');
|
|
589
|
+
expect(audio).not.toBe(null);
|
|
590
|
+
await expect.element(audio!).toBeVisible();
|
|
591
|
+
|
|
592
|
+
const videoRes = await fetch('/cat.mp4', { method: 'GET' });
|
|
593
|
+
const videoBlob = await videoRes.blob();
|
|
594
|
+
await user.upload(
|
|
595
|
+
screen.getByTestId('insert-media'),
|
|
596
|
+
new File([videoBlob], 'cat.mp4', { type: 'video/mp4' }),
|
|
597
|
+
);
|
|
598
|
+
|
|
599
|
+
// the video should be visible in the editor
|
|
600
|
+
const video = screen.container.querySelector('video');
|
|
601
|
+
expect(video).not.toBe(null);
|
|
602
|
+
await expect.element(video!).toBeVisible();
|
|
603
|
+
|
|
604
|
+
const files = testPost.get('files');
|
|
605
|
+
expect(files.values().length).toBe(3);
|
|
606
|
+
|
|
607
|
+
// delete all media nodes; the files should also be removed from Verdant.
|
|
608
|
+
await user.keyboard('{Delete}{Delete}{Delete}');
|
|
609
|
+
await expect
|
|
610
|
+
.element(screen.getByTestId('editor'))
|
|
611
|
+
.toHaveTextContent('Hello, world!');
|
|
612
|
+
expect(files.values().length).toBe(0);
|
|
400
613
|
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Extension } from '@tiptap/core';
|
|
2
|
+
|
|
3
|
+
import { Plugin, PluginKey } from '@tiptap/pm/state';
|
|
4
|
+
import { id } from '@verdant-web/store';
|
|
5
|
+
|
|
6
|
+
const NodeIdPlugin = new Plugin({
|
|
7
|
+
key: new PluginKey('node-ids'),
|
|
8
|
+
appendTransaction: (_, oldState, newState) => {
|
|
9
|
+
// no changes
|
|
10
|
+
if (newState.doc === oldState.doc) return;
|
|
11
|
+
const tr = newState.tr;
|
|
12
|
+
// force replacement of any duplicates, too
|
|
13
|
+
const usedIds = new Set<string>();
|
|
14
|
+
newState.doc.descendants((node, pos) => {
|
|
15
|
+
if (
|
|
16
|
+
!node.isText &&
|
|
17
|
+
(!node.attrs.id || usedIds.has(node.attrs.id)) &&
|
|
18
|
+
node !== newState.doc
|
|
19
|
+
) {
|
|
20
|
+
const nodeId = id();
|
|
21
|
+
try {
|
|
22
|
+
tr.setNodeMarkup(pos, null, {
|
|
23
|
+
...node.attrs,
|
|
24
|
+
id: nodeId,
|
|
25
|
+
});
|
|
26
|
+
usedIds.add(nodeId);
|
|
27
|
+
} catch (err) {
|
|
28
|
+
console.error('Error assigning node ID', err);
|
|
29
|
+
}
|
|
30
|
+
} else if (node.attrs?.id) {
|
|
31
|
+
usedIds.add(node.attrs.id);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
return tr;
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
export const NodeIdExtension = Extension.create({
|
|
38
|
+
name: 'nodeId',
|
|
39
|
+
addProseMirrorPlugins() {
|
|
40
|
+
return [NodeIdPlugin];
|
|
41
|
+
},
|
|
42
|
+
addOptions() {
|
|
43
|
+
return {
|
|
44
|
+
types: [],
|
|
45
|
+
};
|
|
46
|
+
},
|
|
47
|
+
addGlobalAttributes() {
|
|
48
|
+
return [
|
|
49
|
+
{
|
|
50
|
+
types: this.options.types,
|
|
51
|
+
attributes: {
|
|
52
|
+
id: {
|
|
53
|
+
default: null,
|
|
54
|
+
keepOnSplit: false,
|
|
55
|
+
parseHTML: (element) => element.getAttribute('data-id'),
|
|
56
|
+
renderHTML: (attributes) => {
|
|
57
|
+
if (!attributes.id) return {};
|
|
58
|
+
return {
|
|
59
|
+
'data-id': attributes.id,
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
];
|
|
66
|
+
},
|
|
67
|
+
});
|
|
@@ -1,75 +1,6 @@
|
|
|
1
1
|
import { Extension, JSONContent } from '@tiptap/core';
|
|
2
|
-
import { Plugin, PluginKey } from '@tiptap/pm/state';
|
|
3
2
|
import { assignOid, cloneDeep, maybeGetOid } from '@verdant-web/common';
|
|
4
|
-
import {
|
|
5
|
-
AnyEntity,
|
|
6
|
-
getEntityClient,
|
|
7
|
-
id,
|
|
8
|
-
ObjectEntity,
|
|
9
|
-
} from '@verdant-web/store';
|
|
10
|
-
|
|
11
|
-
const NodeIdPlugin = new Plugin({
|
|
12
|
-
key: new PluginKey('node-ids'),
|
|
13
|
-
appendTransaction: (_, oldState, newState) => {
|
|
14
|
-
// no changes
|
|
15
|
-
if (newState.doc === oldState.doc) return;
|
|
16
|
-
const tr = newState.tr;
|
|
17
|
-
// force replacement of any duplicates, too
|
|
18
|
-
const usedIds = new Set<string>();
|
|
19
|
-
newState.doc.descendants((node, pos) => {
|
|
20
|
-
if (
|
|
21
|
-
!node.isText &&
|
|
22
|
-
(!node.attrs.id || usedIds.has(node.attrs.id)) &&
|
|
23
|
-
node !== newState.doc
|
|
24
|
-
) {
|
|
25
|
-
const nodeId = id();
|
|
26
|
-
try {
|
|
27
|
-
tr.setNodeMarkup(pos, null, {
|
|
28
|
-
...node.attrs,
|
|
29
|
-
id: nodeId,
|
|
30
|
-
});
|
|
31
|
-
usedIds.add(nodeId);
|
|
32
|
-
} catch (err) {
|
|
33
|
-
console.error('Error assigning node ID', err);
|
|
34
|
-
}
|
|
35
|
-
} else if (node.attrs?.id) {
|
|
36
|
-
usedIds.add(node.attrs.id);
|
|
37
|
-
}
|
|
38
|
-
});
|
|
39
|
-
return tr;
|
|
40
|
-
},
|
|
41
|
-
});
|
|
42
|
-
export const NodeIdExtension = Extension.create({
|
|
43
|
-
name: 'nodeId',
|
|
44
|
-
addProseMirrorPlugins() {
|
|
45
|
-
return [NodeIdPlugin];
|
|
46
|
-
},
|
|
47
|
-
addOptions() {
|
|
48
|
-
return {
|
|
49
|
-
types: [],
|
|
50
|
-
};
|
|
51
|
-
},
|
|
52
|
-
addGlobalAttributes() {
|
|
53
|
-
return [
|
|
54
|
-
{
|
|
55
|
-
types: this.options.types,
|
|
56
|
-
attributes: {
|
|
57
|
-
id: {
|
|
58
|
-
default: null,
|
|
59
|
-
keepOnSplit: false,
|
|
60
|
-
parseHTML: (element) => element.getAttribute('data-id'),
|
|
61
|
-
renderHTML: (attributes) => {
|
|
62
|
-
if (!attributes.id) return {};
|
|
63
|
-
return {
|
|
64
|
-
'data-id': attributes.id,
|
|
65
|
-
};
|
|
66
|
-
},
|
|
67
|
-
},
|
|
68
|
-
},
|
|
69
|
-
},
|
|
70
|
-
];
|
|
71
|
-
},
|
|
72
|
-
});
|
|
3
|
+
import { AnyEntity, getEntityClient, ObjectEntity } from '@verdant-web/store';
|
|
73
4
|
|
|
74
5
|
export const verdantIdAttribute = 'data-verdant-oid';
|
|
75
6
|
export interface VerdantExtensionOptions {
|
|
@@ -180,7 +111,7 @@ export const VerdantExtension = Extension.create<
|
|
|
180
111
|
const field = parent.get(fieldName) as ObjectEntity<any, any> | null;
|
|
181
112
|
if (field) {
|
|
182
113
|
unsubscribe = field.subscribe('changeDeep', (target, info) => {
|
|
183
|
-
if (!info.isLocal || target === field) {
|
|
114
|
+
if (!field.deleted && (!info.isLocal || target === field)) {
|
|
184
115
|
updateFromField(field);
|
|
185
116
|
}
|
|
186
117
|
});
|
|
@@ -210,7 +141,7 @@ export const VerdantExtension = Extension.create<
|
|
|
210
141
|
} else {
|
|
211
142
|
// re-assign oids to data objects so they can be diffed more effectively
|
|
212
143
|
// against existing data
|
|
213
|
-
|
|
144
|
+
formatSnapshotForVerdantAndAssignOids(newData);
|
|
214
145
|
// printAllOids(newData);
|
|
215
146
|
const client = getEntityClient(value);
|
|
216
147
|
client.batch(this.options.batchConfig).run(() => {
|
|
@@ -264,13 +195,29 @@ export function createVerdantExtension<
|
|
|
264
195
|
});
|
|
265
196
|
}
|
|
266
197
|
|
|
267
|
-
function
|
|
198
|
+
function formatSnapshotForVerdantAndAssignOids(doc: JSONContent) {
|
|
268
199
|
if (doc.attrs?.[verdantIdAttribute] !== undefined) {
|
|
269
200
|
assignOid(doc, doc.attrs[verdantIdAttribute]);
|
|
270
201
|
delete doc.attrs[verdantIdAttribute];
|
|
271
202
|
}
|
|
203
|
+
// make sure all fields are present, even if null.
|
|
204
|
+
if (doc.from === undefined) {
|
|
205
|
+
doc.from = null;
|
|
206
|
+
}
|
|
207
|
+
if (doc.to === undefined) {
|
|
208
|
+
doc.to = null;
|
|
209
|
+
}
|
|
210
|
+
if (doc.text === undefined) {
|
|
211
|
+
doc.text = null as any;
|
|
212
|
+
}
|
|
213
|
+
if (doc.attrs === undefined) {
|
|
214
|
+
doc.attrs = {};
|
|
215
|
+
}
|
|
216
|
+
if (doc.marks === undefined) {
|
|
217
|
+
doc.marks = null as any;
|
|
218
|
+
}
|
|
272
219
|
if (doc.content) {
|
|
273
|
-
doc.content.forEach(
|
|
220
|
+
doc.content.forEach(formatSnapshotForVerdantAndAssignOids);
|
|
274
221
|
}
|
|
275
222
|
}
|
|
276
223
|
|
|
@@ -279,7 +226,7 @@ function consumeOidsAndAssignToSnapshots(doc: JSONContent) {
|
|
|
279
226
|
function ensureDocShape(json: any) {
|
|
280
227
|
for (const node of json.content ?? []) {
|
|
281
228
|
// remove undefined nodes
|
|
282
|
-
node.content = node.content
|
|
229
|
+
node.content = node.content?.filter((n: any) => !!n).map(ensureDocShape);
|
|
283
230
|
}
|
|
284
231
|
return json;
|
|
285
232
|
}
|
|
@@ -311,13 +258,3 @@ function addOidAttrs(doc: JSONContent) {
|
|
|
311
258
|
doc.content.forEach(addOidAttrs);
|
|
312
259
|
}
|
|
313
260
|
}
|
|
314
|
-
|
|
315
|
-
function printAllOids(obj: any) {
|
|
316
|
-
if (obj && typeof obj === 'object') {
|
|
317
|
-
const oid = maybeGetOid(obj);
|
|
318
|
-
if (oid) console.log(oid, obj);
|
|
319
|
-
for (const key in obj) {
|
|
320
|
-
printAllOids(obj[key]);
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
}
|