@manuscripts/track-changes-plugin 0.4.0 → 0.4.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/ChangeSet.d.ts +2 -7
- package/dist/change-steps/diffChangeSteps.d.ts +1 -1
- package/dist/changes/fixInconsistentChanges.d.ts +1 -1
- package/dist/compute/nodeHelpers.d.ts +3 -2
- package/dist/index.cjs +337 -224
- package/dist/index.es.js +1547 -0
- package/dist/index.js +337 -224
- package/dist/mutate/deleteAndMergeSplitNodes.d.ts +1 -2
- package/dist/mutate/deleteNode.d.ts +1 -1
- package/dist/track/applyChanges.d.ts +28 -0
- package/dist/track/deleteNode.d.ts +27 -0
- package/dist/track/findChanges.d.ts +27 -0
- package/dist/track/fixInconsistentChanges.d.ts +29 -0
- package/dist/track/mergeNode.d.ts +25 -0
- package/dist/track/node-utils.d.ts +27 -0
- package/dist/track/steps/deleteAndMergeSplitNodes.d.ts +53 -0
- package/dist/track/steps/mergeTrackedMarks.d.ts +29 -0
- package/dist/track/steps/setFragmentAsInserted.d.ts +18 -0
- package/dist/track/steps/track-utils.d.ts +18 -0
- package/dist/track/steps/trackReplaceAroundStep.d.ts +4 -0
- package/dist/track/steps/trackReplaceStep.d.ts +4 -0
- package/dist/track/trackTransaction.d.ts +17 -0
- package/dist/track/updateChangeAttrs.d.ts +21 -0
- package/dist/types/change.d.ts +5 -5
- package/dist/types/editor.d.ts +23 -0
- package/dist/types/step.d.ts +2 -1
- package/dist/types/track.d.ts +5 -1
- package/dist/utils/track-utils.d.ts +2 -16
- package/package.json +2 -1
package/dist/index.cjs
CHANGED
|
@@ -179,8 +179,8 @@ class ChangeSet {
|
|
|
179
179
|
get changes() {
|
|
180
180
|
const iteratedIds = new Set();
|
|
181
181
|
return __classPrivateFieldGet(this, _ChangeSet_changes, "f").filter((c) => {
|
|
182
|
-
const valid = !iteratedIds.has(c.
|
|
183
|
-
iteratedIds.add(c.
|
|
182
|
+
const valid = !iteratedIds.has(c.dataTracked.id) && ChangeSet.isValidDataTracked(c.dataTracked);
|
|
183
|
+
iteratedIds.add(c.dataTracked.id);
|
|
184
184
|
return valid;
|
|
185
185
|
});
|
|
186
186
|
}
|
|
@@ -215,13 +215,13 @@ class ChangeSet {
|
|
|
215
215
|
return rootNodes;
|
|
216
216
|
}
|
|
217
217
|
get pending() {
|
|
218
|
-
return this.changeTree.filter((c) => c.
|
|
218
|
+
return this.changeTree.filter((c) => c.dataTracked.status === exports.CHANGE_STATUS.pending);
|
|
219
219
|
}
|
|
220
220
|
get accepted() {
|
|
221
|
-
return this.changeTree.filter((c) => c.
|
|
221
|
+
return this.changeTree.filter((c) => c.dataTracked.status === exports.CHANGE_STATUS.accepted);
|
|
222
222
|
}
|
|
223
223
|
get rejected() {
|
|
224
|
-
return this.changeTree.filter((c) => c.
|
|
224
|
+
return this.changeTree.filter((c) => c.dataTracked.status === exports.CHANGE_STATUS.rejected);
|
|
225
225
|
}
|
|
226
226
|
get textChanges() {
|
|
227
227
|
return this.changes.filter((c) => c.type === 'text-change');
|
|
@@ -252,7 +252,7 @@ class ChangeSet {
|
|
|
252
252
|
});
|
|
253
253
|
}
|
|
254
254
|
get hasIncompleteAttrs() {
|
|
255
|
-
return __classPrivateFieldGet(this, _ChangeSet_changes, "f").some((c) => !ChangeSet.
|
|
255
|
+
return __classPrivateFieldGet(this, _ChangeSet_changes, "f").some((c) => !ChangeSet.isValidDataTracked(c.dataTracked));
|
|
256
256
|
}
|
|
257
257
|
get(id) {
|
|
258
258
|
return __classPrivateFieldGet(this, _ChangeSet_changes, "f").find((c) => c.id === id);
|
|
@@ -272,29 +272,22 @@ class ChangeSet {
|
|
|
272
272
|
static flattenTreeToIds(changes) {
|
|
273
273
|
return changes.flatMap((c) => this.isNodeChange(c) ? [c.id, ...c.children.map((c) => c.id)] : c.id);
|
|
274
274
|
}
|
|
275
|
-
/**
|
|
276
|
-
* Determines whether a change should not be deleted when applying it to the document.
|
|
277
|
-
* @param change
|
|
278
|
-
*/
|
|
279
|
-
static shouldNotDelete(change) {
|
|
280
|
-
return !ChangeSet.shouldDeleteChange(change);
|
|
281
|
-
}
|
|
282
275
|
/**
|
|
283
276
|
* Determines whether a change should be deleted when applying it to the document.
|
|
284
277
|
* @param change
|
|
285
278
|
*/
|
|
286
279
|
static shouldDeleteChange(change) {
|
|
287
|
-
const { status, operation } = change.
|
|
280
|
+
const { status, operation } = change.dataTracked;
|
|
288
281
|
return ((operation === exports.CHANGE_OPERATION.insert && status === exports.CHANGE_STATUS.rejected) ||
|
|
289
282
|
(operation === exports.CHANGE_OPERATION.delete && status === exports.CHANGE_STATUS.accepted));
|
|
290
283
|
}
|
|
291
284
|
/**
|
|
292
285
|
* Checks whether change attributes contain all TrackedAttrs keys with non-undefined values
|
|
293
|
-
* @param
|
|
286
|
+
* @param dataTracked
|
|
294
287
|
*/
|
|
295
|
-
static
|
|
296
|
-
if ('
|
|
297
|
-
log.warn('passed "
|
|
288
|
+
static isValidDataTracked(dataTracked = {}) {
|
|
289
|
+
if ('dataTracked' in dataTracked) {
|
|
290
|
+
log.warn('passed "dataTracked" as property to isValidTrackedAttrs()', dataTracked);
|
|
298
291
|
}
|
|
299
292
|
const trackedKeys = [
|
|
300
293
|
'id',
|
|
@@ -307,12 +300,12 @@ class ChangeSet {
|
|
|
307
300
|
// reviewedByID is set optional since either ProseMirror or Yjs doesn't like persisting null values inside attributes objects
|
|
308
301
|
// So it can be either omitted completely or at least be null or string
|
|
309
302
|
const optionalKeys = ['reviewedByID'];
|
|
310
|
-
const entries = Object.entries(
|
|
311
|
-
const optionalEntries = Object.entries(
|
|
303
|
+
const entries = Object.entries(dataTracked).filter(([key, val]) => trackedKeys.includes(key));
|
|
304
|
+
const optionalEntries = Object.entries(dataTracked).filter(([key, val]) => optionalKeys.includes(key));
|
|
312
305
|
return (entries.length === trackedKeys.length &&
|
|
313
306
|
entries.every(([key, val]) => trackedKeys.includes(key) && val !== undefined) &&
|
|
314
307
|
optionalEntries.every(([key, val]) => optionalKeys.includes(key) && val !== undefined) &&
|
|
315
|
-
(
|
|
308
|
+
(dataTracked.id || '').length > 0 // Changes created with undefined id have '' as placeholder
|
|
316
309
|
);
|
|
317
310
|
}
|
|
318
311
|
static isTextChange(change) {
|
|
@@ -358,8 +351,8 @@ function addTrackIdIfDoesntExist(attrs) {
|
|
|
358
351
|
}
|
|
359
352
|
return attrs;
|
|
360
353
|
}
|
|
361
|
-
function
|
|
362
|
-
if (!node || !node.
|
|
354
|
+
function getTextNodeTrackedMarkData(node, schema) {
|
|
355
|
+
if (!node || !node.isText) {
|
|
363
356
|
return undefined;
|
|
364
357
|
}
|
|
365
358
|
const marksTrackedData = [];
|
|
@@ -376,12 +369,25 @@ function getInlineNodeTrackedMarkData(node, schema) {
|
|
|
376
369
|
}
|
|
377
370
|
return marksTrackedData[0] || undefined;
|
|
378
371
|
}
|
|
372
|
+
function getBlockInlineTrackedData(node) {
|
|
373
|
+
let { dataTracked } = node.attrs;
|
|
374
|
+
if (dataTracked && !Array.isArray(dataTracked)) {
|
|
375
|
+
return [dataTracked];
|
|
376
|
+
}
|
|
377
|
+
return dataTracked;
|
|
378
|
+
}
|
|
379
379
|
function getNodeTrackedData(node, schema) {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
380
|
+
let tracked;
|
|
381
|
+
if (node && !node.isText) {
|
|
382
|
+
tracked = getBlockInlineTrackedData(node);
|
|
383
|
+
}
|
|
384
|
+
else if (node === null || node === void 0 ? void 0 : node.isText) {
|
|
385
|
+
tracked = getTextNodeTrackedMarkData(node, schema);
|
|
386
|
+
}
|
|
387
|
+
if (tracked && !Array.isArray(tracked)) {
|
|
388
|
+
tracked = [tracked];
|
|
389
|
+
}
|
|
390
|
+
return tracked;
|
|
385
391
|
}
|
|
386
392
|
function equalMarks(n1, n2) {
|
|
387
393
|
return (n1.marks.length === n2.marks.length &&
|
|
@@ -400,7 +406,7 @@ function shouldMergeTrackedAttributes(left, right) {
|
|
|
400
406
|
left.authorID === right.authorID);
|
|
401
407
|
}
|
|
402
408
|
function getMergeableMarkTrackedAttrs(node, attrs, schema) {
|
|
403
|
-
const nodeAttrs =
|
|
409
|
+
const nodeAttrs = getTextNodeTrackedMarkData(node, schema);
|
|
404
410
|
return nodeAttrs && shouldMergeTrackedAttributes(nodeAttrs, attrs) ? nodeAttrs : null;
|
|
405
411
|
}
|
|
406
412
|
|
|
@@ -449,6 +455,29 @@ function deleteNode(node, pos, tr) {
|
|
|
449
455
|
// child, say some wrapper blockNode, is also deleted the content could be retained. TODO I guess.
|
|
450
456
|
return tr.delete(pos, pos + node.nodeSize);
|
|
451
457
|
}
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Deletes inserted block or inline node, otherwise adds `dataTracked` object with CHANGE_STATUS 'deleted'
|
|
461
|
+
* @param node
|
|
462
|
+
* @param pos
|
|
463
|
+
* @param newTr
|
|
464
|
+
* @param deleteAttrs
|
|
465
|
+
*/
|
|
466
|
+
function deleteOrSetNodeDeleted(node, pos, newTr, deleteAttrs) {
|
|
467
|
+
const dataTracked = getBlockInlineTrackedData(node);
|
|
468
|
+
const inserted = dataTracked === null || dataTracked === void 0 ? void 0 : dataTracked.find((d) => d.operation === exports.CHANGE_OPERATION.insert);
|
|
469
|
+
const deleted = dataTracked === null || dataTracked === void 0 ? void 0 : dataTracked.find((d) => d.operation === exports.CHANGE_OPERATION.delete);
|
|
470
|
+
const updated = dataTracked === null || dataTracked === void 0 ? void 0 : dataTracked.find((d) => d.operation === exports.CHANGE_OPERATION.set_node_attributes);
|
|
471
|
+
if (inserted && inserted.authorID === deleteAttrs.authorID) {
|
|
472
|
+
return deleteNode(node, pos, newTr);
|
|
473
|
+
}
|
|
474
|
+
const newDeleted = deleted
|
|
475
|
+
? { ...deleted, updatedAt: deleteAttrs.updatedAt }
|
|
476
|
+
: addTrackIdIfDoesntExist(deleteAttrs);
|
|
477
|
+
newTr.setNodeMarkup(pos, undefined, {
|
|
478
|
+
...node.attrs,
|
|
479
|
+
dataTracked: updated ? [newDeleted, updated] : [newDeleted],
|
|
480
|
+
}, node.marks);
|
|
452
481
|
}
|
|
453
482
|
|
|
454
483
|
/*!
|
|
@@ -494,21 +523,46 @@ function mergeNode(node, pos, tr) {
|
|
|
494
523
|
function updateChangeAttrs(tr, change, trackedAttrs, schema) {
|
|
495
524
|
const node = tr.doc.nodeAt(change.from);
|
|
496
525
|
if (!node) {
|
|
497
|
-
|
|
526
|
+
log.error('updateChangeAttrs: no node at the from of change ', change);
|
|
527
|
+
return tr;
|
|
528
|
+
}
|
|
529
|
+
const { operation } = trackedAttrs;
|
|
530
|
+
const oldTrackData = change.type === 'text-change'
|
|
531
|
+
? getTextNodeTrackedMarkData(node, schema)
|
|
532
|
+
: getBlockInlineTrackedData(node);
|
|
533
|
+
if (!operation) {
|
|
534
|
+
log.warn('updateChangeAttrs: unable to determine operation of change ', change);
|
|
535
|
+
}
|
|
536
|
+
else if (!oldTrackData) {
|
|
537
|
+
log.warn('updateChangeAttrs: no old dataTracked for change ', change);
|
|
538
|
+
}
|
|
539
|
+
if (change.type === 'text-change') {
|
|
540
|
+
const oldMark = node.marks.find((m) => m.type === schema.marks.tracked_insert || m.type === schema.marks.tracked_delete);
|
|
541
|
+
if (!oldMark) {
|
|
542
|
+
log.warn('updateChangeAttrs: no track marks for a text-change ', change);
|
|
543
|
+
return tr;
|
|
544
|
+
}
|
|
545
|
+
// TODO add operation based on mark type if it's undefined?
|
|
546
|
+
tr.addMark(change.from, change.to, oldMark.type.create({ ...oldMark.attrs, dataTracked: { ...oldTrackData, ...trackedAttrs } }));
|
|
498
547
|
}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
tr.addMark(change.from, change.to, oldMark.type.create({ ...oldMark.attrs, dataTracked }));
|
|
548
|
+
else if (change.type === 'node-change' && !operation) {
|
|
549
|
+
// Very weird edge-case if this happens
|
|
550
|
+
tr.setNodeMarkup(change.from, undefined, { ...node.attrs, dataTracked: null }, node.marks);
|
|
503
551
|
}
|
|
504
552
|
else if (change.type === 'node-change') {
|
|
505
|
-
|
|
553
|
+
const newDataTracked = (getBlockInlineTrackedData(node) || []).map((oldTrack) => {
|
|
554
|
+
if (oldTrack.operation === operation) {
|
|
555
|
+
return { ...oldTrack, ...trackedAttrs };
|
|
556
|
+
}
|
|
557
|
+
return oldTrack;
|
|
558
|
+
});
|
|
559
|
+
tr.setNodeMarkup(change.from, undefined, { ...node.attrs, dataTracked: newDataTracked.length === 0 ? null : newDataTracked }, node.marks);
|
|
506
560
|
}
|
|
507
561
|
return tr;
|
|
508
562
|
}
|
|
509
563
|
function updateChangeChildrenAttributes(changes, tr, mapping) {
|
|
510
564
|
changes.forEach((c) => {
|
|
511
|
-
if (c.type === 'node-change' && ChangeSet.
|
|
565
|
+
if (c.type === 'node-change' && !ChangeSet.shouldDeleteChange(c)) {
|
|
512
566
|
const from = mapping.map(c.from);
|
|
513
567
|
const node = tr.doc.nodeAt(from);
|
|
514
568
|
if (!node) {
|
|
@@ -530,12 +584,12 @@ function updateChangeChildrenAttributes(changes, tr, mapping) {
|
|
|
530
584
|
*/
|
|
531
585
|
function applyAcceptedRejectedChanges(tr, schema, changes, deleteMap = new prosemirrorTransform.Mapping()) {
|
|
532
586
|
changes.forEach((change) => {
|
|
533
|
-
if (change.
|
|
587
|
+
if (change.dataTracked.status === exports.CHANGE_STATUS.pending) {
|
|
534
588
|
return;
|
|
535
589
|
}
|
|
536
590
|
// Map change.from and skip those which dont need to be applied
|
|
537
591
|
// or were already deleted by an applied block delete
|
|
538
|
-
const { pos: from, deleted } = deleteMap.mapResult(change.from), node = tr.doc.nodeAt(from), noChangeNeeded = deleted || ChangeSet.
|
|
592
|
+
const { pos: from, deleted } = deleteMap.mapResult(change.from), node = tr.doc.nodeAt(from), noChangeNeeded = deleted || !ChangeSet.shouldDeleteChange(change);
|
|
539
593
|
if (!node) {
|
|
540
594
|
!deleted && log.warn('no node found to update for change', change);
|
|
541
595
|
return;
|
|
@@ -564,12 +618,12 @@ function applyAcceptedRejectedChanges(tr, schema, changes, deleteMap = new prose
|
|
|
564
618
|
deleteMap.appendMap(tr.steps[tr.steps.length - 1].getMap());
|
|
565
619
|
}
|
|
566
620
|
else if (ChangeSet.isNodeAttrChange(change) &&
|
|
567
|
-
change.
|
|
621
|
+
change.dataTracked.status === exports.CHANGE_STATUS.accepted) {
|
|
568
622
|
const attrs = { ...node.attrs, dataTracked: null };
|
|
569
623
|
tr.setNodeMarkup(from, undefined, attrs, node.marks);
|
|
570
624
|
}
|
|
571
625
|
else if (ChangeSet.isNodeAttrChange(change) &&
|
|
572
|
-
change.
|
|
626
|
+
change.dataTracked.status === exports.CHANGE_STATUS.rejected) {
|
|
573
627
|
const attrs = { ...change.oldAttrs, dataTracked: null };
|
|
574
628
|
tr.setNodeMarkup(from, undefined, attrs, node.marks);
|
|
575
629
|
}
|
|
@@ -591,9 +645,10 @@ function findChanges(state) {
|
|
|
591
645
|
// Store the last iterated change to join adjacent text changes
|
|
592
646
|
let current;
|
|
593
647
|
state.doc.descendants((node, pos) => {
|
|
594
|
-
const
|
|
595
|
-
|
|
596
|
-
const
|
|
648
|
+
const tracked = getNodeTrackedData(node, state.schema) || [];
|
|
649
|
+
for (let i = 0; i < tracked.length; i += 1) {
|
|
650
|
+
const dataTracked = tracked[i];
|
|
651
|
+
const id = dataTracked.id || '';
|
|
597
652
|
// Join adjacent text changes that have been broken up due to different marks
|
|
598
653
|
// eg <ins><b>bold</b>norm<i>italic</i></ins> -> treated as one continuous change
|
|
599
654
|
// Note the !equalMarks to leave changes separate incase the marks are equal to let fixInconsistentChanges to fix them
|
|
@@ -605,38 +660,49 @@ function findChanges(state) {
|
|
|
605
660
|
current.change.to = pos + node.nodeSize;
|
|
606
661
|
// Important to update the node as the text changes might contain multiple parts where some marks equal each other
|
|
607
662
|
current.node = node;
|
|
663
|
+
continue;
|
|
608
664
|
}
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
665
|
+
current && changes.push(current.change);
|
|
666
|
+
let change;
|
|
667
|
+
if (node.isText) {
|
|
668
|
+
change = {
|
|
669
|
+
id,
|
|
670
|
+
type: 'text-change',
|
|
671
|
+
from: pos,
|
|
672
|
+
to: pos + node.nodeSize,
|
|
673
|
+
dataTracked,
|
|
674
|
+
text: node.text,
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
else if (dataTracked.operation === exports.CHANGE_OPERATION.set_node_attributes) {
|
|
678
|
+
change = {
|
|
679
|
+
id,
|
|
680
|
+
type: 'node-attr-change',
|
|
681
|
+
from: pos,
|
|
682
|
+
to: pos + node.nodeSize,
|
|
683
|
+
dataTracked,
|
|
684
|
+
nodeType: node.type.name,
|
|
685
|
+
newAttrs: node.attrs,
|
|
686
|
+
oldAttrs: dataTracked.oldAttrs,
|
|
621
687
|
};
|
|
622
688
|
}
|
|
623
689
|
else {
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
children: [],
|
|
633
|
-
attrs,
|
|
634
|
-
},
|
|
635
|
-
node,
|
|
690
|
+
change = {
|
|
691
|
+
id,
|
|
692
|
+
type: 'node-change',
|
|
693
|
+
from: pos,
|
|
694
|
+
to: pos + node.nodeSize,
|
|
695
|
+
dataTracked,
|
|
696
|
+
nodeType: node.type.name,
|
|
697
|
+
children: [],
|
|
636
698
|
};
|
|
637
699
|
}
|
|
700
|
+
current = {
|
|
701
|
+
change,
|
|
702
|
+
node,
|
|
703
|
+
};
|
|
638
704
|
}
|
|
639
|
-
|
|
705
|
+
if (tracked.length === 0 && current) {
|
|
640
706
|
changes.push(current.change);
|
|
641
707
|
current = undefined;
|
|
642
708
|
}
|
|
@@ -655,22 +721,23 @@ function findChanges(state) {
|
|
|
655
721
|
* @param schema
|
|
656
722
|
* @return docWasChanged, a boolean
|
|
657
723
|
*/
|
|
658
|
-
function fixInconsistentChanges(changeSet,
|
|
724
|
+
function fixInconsistentChanges(changeSet, currentUserID, newTr, schema) {
|
|
659
725
|
const iteratedIds = new Set();
|
|
660
726
|
let changed = false;
|
|
661
727
|
changeSet.invalidChanges.forEach((c) => {
|
|
662
|
-
const { id, authorID, operation, reviewedByID, status, createdAt, updatedAt } = c.
|
|
728
|
+
const { id, authorID, operation, reviewedByID, status, createdAt, updatedAt } = c.dataTracked;
|
|
663
729
|
const newAttrs = {
|
|
664
730
|
...((!id || iteratedIds.has(id) || id.length === 0) && { id: uuidv4() }),
|
|
665
|
-
...(!authorID && { authorID:
|
|
666
|
-
|
|
731
|
+
...(!authorID && { authorID: currentUserID }),
|
|
732
|
+
// Dont add a default operation -> rather have updateChangeAttrs delete the track data
|
|
733
|
+
// ...(!operation && { operation: CHANGE_OPERATION.insert }),
|
|
667
734
|
...(!reviewedByID && { reviewedByID: null }),
|
|
668
735
|
...(!status && { status: exports.CHANGE_STATUS.pending }),
|
|
669
736
|
...(!createdAt && { createdAt: Date.now() }),
|
|
670
737
|
...(!updatedAt && { updatedAt: Date.now() }),
|
|
671
738
|
};
|
|
672
739
|
if (Object.keys(newAttrs).length > 0) {
|
|
673
|
-
updateChangeAttrs(newTr, c, { ...c.
|
|
740
|
+
updateChangeAttrs(newTr, c, { ...c.dataTracked, ...newAttrs }, schema);
|
|
674
741
|
changed = true;
|
|
675
742
|
}
|
|
676
743
|
iteratedIds.add(newAttrs.id || id);
|
|
@@ -800,18 +867,43 @@ function markInlineNodeChange(node, newTrackAttrs, schema) {
|
|
|
800
867
|
});
|
|
801
868
|
return node.mark(filtered.concat(createdMark));
|
|
802
869
|
}
|
|
870
|
+
/**
|
|
871
|
+
* Iterates over fragment's content and joins pasted text with old track marks
|
|
872
|
+
*
|
|
873
|
+
* This is not strictly necessary but it's kinda bad UX if the inserted text is split into parts
|
|
874
|
+
* even when it's authored by the same user.
|
|
875
|
+
* @param content
|
|
876
|
+
* @param newTrackAttrs
|
|
877
|
+
* @param schema
|
|
878
|
+
* @returns
|
|
879
|
+
*/
|
|
880
|
+
function loopContentAndMergeText(content, newTrackAttrs, schema) {
|
|
881
|
+
var _a;
|
|
882
|
+
const updatedChildren = [];
|
|
883
|
+
for (let i = 0; i < content.childCount; i += 1) {
|
|
884
|
+
const recursed = recurseNodeContent(content.child(i), newTrackAttrs, schema);
|
|
885
|
+
const prev = i > 0 ? updatedChildren[i - 1] : null;
|
|
886
|
+
if ((prev === null || prev === void 0 ? void 0 : prev.isText) &&
|
|
887
|
+
recursed.isText &&
|
|
888
|
+
equalMarks(prev, recursed) &&
|
|
889
|
+
((_a = getTextNodeTrackedMarkData(prev, schema)) === null || _a === void 0 ? void 0 : _a.operation) === exports.CHANGE_OPERATION.insert) {
|
|
890
|
+
updatedChildren.splice(i - 1, 1, schema.text('' + prev.text + recursed.text, prev.marks));
|
|
891
|
+
}
|
|
892
|
+
else {
|
|
893
|
+
updatedChildren.push(recursed);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
return updatedChildren;
|
|
897
|
+
}
|
|
803
898
|
function recurseNodeContent(node, newTrackAttrs, schema) {
|
|
804
899
|
if (node.isText) {
|
|
805
900
|
return markInlineNodeChange(node, newTrackAttrs, schema);
|
|
806
901
|
}
|
|
807
902
|
else if (node.isBlock || node.isInline) {
|
|
808
|
-
const updatedChildren =
|
|
809
|
-
node.content.forEach((child) => {
|
|
810
|
-
updatedChildren.push(recurseNodeContent(child, newTrackAttrs, schema));
|
|
811
|
-
});
|
|
903
|
+
const updatedChildren = loopContentAndMergeText(node.content, newTrackAttrs, schema);
|
|
812
904
|
return node.type.create({
|
|
813
905
|
...node.attrs,
|
|
814
|
-
dataTracked: addTrackIdIfDoesntExist(newTrackAttrs),
|
|
906
|
+
dataTracked: [addTrackIdIfDoesntExist(newTrackAttrs)],
|
|
815
907
|
}, prosemirrorModel.Fragment.fromArray(updatedChildren), node.marks);
|
|
816
908
|
}
|
|
817
909
|
else {
|
|
@@ -821,11 +913,8 @@ function recurseNodeContent(node, newTrackAttrs, schema) {
|
|
|
821
913
|
}
|
|
822
914
|
function setFragmentAsInserted(inserted, insertAttrs, schema) {
|
|
823
915
|
// Recurse the content in the inserted slice and either mark it tracked_insert or set node attrs
|
|
824
|
-
const updatedInserted =
|
|
825
|
-
|
|
826
|
-
updatedInserted.push(recurseNodeContent(n, insertAttrs, schema));
|
|
827
|
-
});
|
|
828
|
-
return updatedInserted.length === 0 ? inserted : prosemirrorModel.Fragment.fromArray(updatedInserted);
|
|
916
|
+
const updatedInserted = loopContentAndMergeText(inserted, insertAttrs, schema);
|
|
917
|
+
return updatedInserted.length === 0 ? prosemirrorModel.Fragment.empty : prosemirrorModel.Fragment.fromArray(updatedInserted);
|
|
829
918
|
}
|
|
830
919
|
|
|
831
920
|
/*!
|
|
@@ -854,6 +943,13 @@ function createNewDeleteAttrs(attrs) {
|
|
|
854
943
|
...attrs,
|
|
855
944
|
operation: exports.CHANGE_OPERATION.delete,
|
|
856
945
|
};
|
|
946
|
+
}
|
|
947
|
+
function createNewUpdateAttrs(attrs, oldAttrs) {
|
|
948
|
+
return {
|
|
949
|
+
...attrs,
|
|
950
|
+
operation: exports.CHANGE_OPERATION.set_node_attributes,
|
|
951
|
+
oldAttrs,
|
|
952
|
+
};
|
|
857
953
|
}
|
|
858
954
|
|
|
859
955
|
/*!
|
|
@@ -897,13 +993,12 @@ function createNewDeleteAttrs(attrs) {
|
|
|
897
993
|
* @returns mapping adjusted by the applied operations & modified insert slice
|
|
898
994
|
*/
|
|
899
995
|
function deleteAndMergeSplitNodes(from, to, gap, startDoc, newTr, schema, trackAttrs, insertSlice) {
|
|
900
|
-
const deleteMap = new prosemirrorTransform.Mapping();
|
|
901
996
|
const steps = [];
|
|
902
997
|
// No deletion applied, return default values
|
|
903
998
|
if (from === to) {
|
|
904
999
|
return {
|
|
905
|
-
deleteMap,
|
|
906
1000
|
newSliceContent: insertSlice.content,
|
|
1001
|
+
sliceWasSplit: false,
|
|
907
1002
|
steps,
|
|
908
1003
|
};
|
|
909
1004
|
}
|
|
@@ -911,10 +1006,7 @@ function deleteAndMergeSplitNodes(from, to, gap, startDoc, newTr, schema, trackA
|
|
|
911
1006
|
const { updatedSliceNodes, firstMergedNode, lastMergedNode } = splitSliceIntoMergedParts(insertSlice, gap !== undefined);
|
|
912
1007
|
let mergingStartSide = true;
|
|
913
1008
|
startDoc.nodesBetween(from, to, (node, pos) => {
|
|
914
|
-
const
|
|
915
|
-
const offsetFrom = deleteMap.map(from, -1);
|
|
916
|
-
const offsetTo = deleteMap.map(to, 1);
|
|
917
|
-
const nodeEnd = offsetPos + node.nodeSize;
|
|
1009
|
+
const nodeEnd = pos + node.nodeSize;
|
|
918
1010
|
// So this insane boolean checks for ReplaceAroundStep gaps and whether the node should be skipped
|
|
919
1011
|
// since the content inside gap should stay unchanged.
|
|
920
1012
|
// All other nodes except text nodes consist of one start and end token (or just a single token for atoms).
|
|
@@ -925,23 +1017,13 @@ function deleteAndMergeSplitNodes(from, to, gap, startDoc, newTr, schema, trackA
|
|
|
925
1017
|
// are altered and should not be skipped.
|
|
926
1018
|
// @TODO ATM 20.7.2022 there doesn't seem to be tests that capture this.
|
|
927
1019
|
const wasWithinGap = gap &&
|
|
928
|
-
((!node.isText &&
|
|
929
|
-
(node.isText &&
|
|
930
|
-
offsetPos <= deleteMap.map(gap.start, -1) &&
|
|
931
|
-
nodeEnd >= deleteMap.map(gap.end, -1)));
|
|
932
|
-
let step = newTr.steps[newTr.steps.length - 1];
|
|
1020
|
+
((!node.isText && pos >= gap.start) ||
|
|
1021
|
+
(node.isText && pos <= gap.start && nodeEnd >= gap.start));
|
|
933
1022
|
// nodeEnd > offsetFrom -> delete touches this node
|
|
934
1023
|
// eg (del 6 10) <p 5>|<t 6>cdf</t 9></p 10>| -> <p> nodeEnd 10 > from 6
|
|
935
|
-
|
|
936
|
-
// !nodeWasDeleted -> Check node wasn't already deleted by a previous deleteNode
|
|
937
|
-
// This is quite tricky to wrap your head around and I've forgotten the nitty-gritty details already.
|
|
938
|
-
// But from what I remember what it safeguards against is, when you've already deleted a node
|
|
939
|
-
// say an inserted blockquote that had all its children deleted, nodesBetween still iterates over those
|
|
940
|
-
// nodes and therefore we have to make this check to ensure they still exist in the doc.
|
|
941
|
-
//
|
|
942
|
-
if (nodeEnd > offsetFrom && !nodeWasDeleted && !wasWithinGap) {
|
|
1024
|
+
if (nodeEnd > from && !wasWithinGap) {
|
|
943
1025
|
// |<p>asdf</p>| -> node deleted completely
|
|
944
|
-
const nodeCompletelyDeleted =
|
|
1026
|
+
const nodeCompletelyDeleted = pos >= from && nodeEnd <= to;
|
|
945
1027
|
// The end token deleted eg:
|
|
946
1028
|
// <p 1>asdf|</p 7><p 7>bye</p 12>| + [<p>]hello</p> -> <p>asdfhello</p>
|
|
947
1029
|
// (del 6 12) + (ins [<p>]hello</p> openStart 1 openEnd 0)
|
|
@@ -952,14 +1034,14 @@ function deleteAndMergeSplitNodes(from, to, gap, startDoc, newTr, schema, trackA
|
|
|
952
1034
|
//
|
|
953
1035
|
// What about:
|
|
954
1036
|
// <p 1>asdf|</p 7><p 7 op="inserted">|bye</p 12> + empty -> <p>asdfbye</p>
|
|
955
|
-
const endTokenDeleted = nodeEnd <=
|
|
1037
|
+
const endTokenDeleted = nodeEnd <= to;
|
|
956
1038
|
// The start token deleted eg:
|
|
957
1039
|
// |<p1 0>hey</p 6><p2 6>|asdf</p 12> + <p3>hello [</p>] -> <p3>hello asdf</p2>
|
|
958
1040
|
// (del 0 7) + (ins <p>hello [</p>] openStart 0 openEnd 1)
|
|
959
1041
|
// (<p1> pos 0) >= (from 0) && (nodeEnd 6) - 1 > (to 7) == false???
|
|
960
1042
|
// (<p2> pos 6) >= (from 0) && (nodeEnd 12) - 1 > (to 7) == true
|
|
961
1043
|
//
|
|
962
|
-
const startTokenDeleted =
|
|
1044
|
+
const startTokenDeleted = pos >= from; // && nodeEnd - 1 > offsetTo
|
|
963
1045
|
if (node.isText ||
|
|
964
1046
|
(!endTokenDeleted && startTokenDeleted) ||
|
|
965
1047
|
(endTokenDeleted && !startTokenDeleted)) {
|
|
@@ -971,7 +1053,7 @@ function deleteAndMergeSplitNodes(from, to, gap, startDoc, newTr, schema, trackA
|
|
|
971
1053
|
}
|
|
972
1054
|
// Depth is often 1 when merging paragraphs or 2 for fully open blockquotes.
|
|
973
1055
|
// Incase of merging text within a ReplaceAroundStep the depth might be 1
|
|
974
|
-
const depth = newTr.doc.resolve(
|
|
1056
|
+
const depth = newTr.doc.resolve(pos).depth;
|
|
975
1057
|
const mergeContent = mergingStartSide
|
|
976
1058
|
? firstMergedNode === null || firstMergedNode === void 0 ? void 0 : firstMergedNode.mergedNodeContent
|
|
977
1059
|
: lastMergedNode === null || lastMergedNode === void 0 ? void 0 : lastMergedNode.mergedNodeContent;
|
|
@@ -989,10 +1071,10 @@ function deleteAndMergeSplitNodes(from, to, gap, startDoc, newTr, schema, trackA
|
|
|
989
1071
|
// the paragraph.
|
|
990
1072
|
steps.push({
|
|
991
1073
|
type: 'merge-fragment',
|
|
992
|
-
pos
|
|
993
|
-
mergePos: mergeStartNode ? nodeEnd - openStart :
|
|
994
|
-
from
|
|
995
|
-
to
|
|
1074
|
+
pos,
|
|
1075
|
+
mergePos: mergeStartNode ? nodeEnd - openStart : pos + openEnd,
|
|
1076
|
+
from,
|
|
1077
|
+
to,
|
|
996
1078
|
node,
|
|
997
1079
|
fragment: setFragmentAsInserted(mergeContent, createNewInsertAttrs(trackAttrs), schema),
|
|
998
1080
|
});
|
|
@@ -1010,9 +1092,9 @@ function deleteAndMergeSplitNodes(from, to, gap, startDoc, newTr, schema, trackA
|
|
|
1010
1092
|
// (which is basically the case most of the time)
|
|
1011
1093
|
steps.push({
|
|
1012
1094
|
type: 'delete-text',
|
|
1013
|
-
pos
|
|
1014
|
-
from: Math.max(
|
|
1015
|
-
to: Math.min(nodeEnd,
|
|
1095
|
+
pos,
|
|
1096
|
+
from: Math.max(pos, from),
|
|
1097
|
+
to: Math.min(nodeEnd, to),
|
|
1016
1098
|
node,
|
|
1017
1099
|
});
|
|
1018
1100
|
}
|
|
@@ -1021,19 +1103,15 @@ function deleteAndMergeSplitNodes(from, to, gap, startDoc, newTr, schema, trackA
|
|
|
1021
1103
|
else if (nodeCompletelyDeleted) {
|
|
1022
1104
|
steps.push({
|
|
1023
1105
|
type: 'delete-node',
|
|
1024
|
-
pos
|
|
1025
|
-
nodeEnd
|
|
1106
|
+
pos,
|
|
1107
|
+
nodeEnd,
|
|
1026
1108
|
node,
|
|
1027
1109
|
});
|
|
1028
1110
|
}
|
|
1029
1111
|
}
|
|
1030
|
-
const newestStep = newTr.steps[newTr.steps.length - 1];
|
|
1031
|
-
if (step !== newestStep) {
|
|
1032
|
-
deleteMap.appendMap(newestStep.getMap());
|
|
1033
|
-
}
|
|
1034
1112
|
});
|
|
1035
1113
|
return {
|
|
1036
|
-
|
|
1114
|
+
sliceWasSplit: !!(firstMergedNode || lastMergedNode),
|
|
1037
1115
|
newSliceContent: updatedSliceNodes
|
|
1038
1116
|
? prosemirrorModel.Fragment.fromArray(updatedSliceNodes)
|
|
1039
1117
|
: insertSlice.content,
|
|
@@ -1127,7 +1205,7 @@ function trackReplaceAroundStep(step, oldState, newTr, attrs) {
|
|
|
1127
1205
|
log.info('RETAINED GAP CONTENT', gap);
|
|
1128
1206
|
// First apply the deleted range and update the insert slice to not include content that was deleted,
|
|
1129
1207
|
// eg partial nodes in an open-ended slice
|
|
1130
|
-
const {
|
|
1208
|
+
const { sliceWasSplit, newSliceContent, steps: deleteSteps, } = deleteAndMergeSplitNodes(from, to, { start: gapFrom, end: gapTo }, newTr.doc, newTr, oldState.schema, attrs, slice);
|
|
1131
1209
|
let steps = deleteSteps;
|
|
1132
1210
|
log.info('TR: new steps after applying delete', [...newTr.steps]);
|
|
1133
1211
|
log.info('DELETE STEPS: ', deleteSteps);
|
|
@@ -1147,15 +1225,16 @@ function trackReplaceAroundStep(step, oldState, newTr, attrs) {
|
|
|
1147
1225
|
}
|
|
1148
1226
|
deleteSteps.push({
|
|
1149
1227
|
type: 'insert-slice',
|
|
1150
|
-
from:
|
|
1151
|
-
to:
|
|
1228
|
+
from: gapFrom,
|
|
1229
|
+
to: gapTo,
|
|
1152
1230
|
slice: insertedSlice,
|
|
1231
|
+
sliceWasSplit,
|
|
1153
1232
|
});
|
|
1154
1233
|
}
|
|
1155
1234
|
else {
|
|
1156
1235
|
// Incase only deletion was applied, check whether tracked marks around deleted content can be merged
|
|
1157
|
-
mergeTrackedMarks(
|
|
1158
|
-
mergeTrackedMarks(
|
|
1236
|
+
mergeTrackedMarks(gapFrom, newTr.doc, newTr, oldState.schema);
|
|
1237
|
+
mergeTrackedMarks(gapTo, newTr.doc, newTr, oldState.schema);
|
|
1159
1238
|
}
|
|
1160
1239
|
return steps;
|
|
1161
1240
|
}
|
|
@@ -1191,11 +1270,11 @@ function trackReplaceStep(step, oldState, newTr, attrs) {
|
|
|
1191
1270
|
log.info('TR: steps before applying delete', [...newTr.steps]);
|
|
1192
1271
|
// First apply the deleted range and update the insert slice to not include content that was deleted,
|
|
1193
1272
|
// eg partial nodes in an open-ended slice
|
|
1194
|
-
const {
|
|
1273
|
+
const { sliceWasSplit, newSliceContent, steps: deleteSteps, } = deleteAndMergeSplitNodes(fromA, toA, undefined, oldState.doc, newTr, oldState.schema, attrs, slice);
|
|
1195
1274
|
changeSteps.push(...deleteSteps);
|
|
1196
1275
|
log.info('TR: steps after applying delete', [...newTr.steps]);
|
|
1197
1276
|
log.info('DELETE STEPS: ', changeSteps);
|
|
1198
|
-
const adjustedInsertPos = deleteMap.map(toA)
|
|
1277
|
+
const adjustedInsertPos = toA; // deleteMap.map(toA)
|
|
1199
1278
|
if (newSliceContent.size > 0) {
|
|
1200
1279
|
log.info('newSliceContent', newSliceContent);
|
|
1201
1280
|
// Since deleteAndMergeSplitBlockNodes modified the slice to not to contain any merged nodes,
|
|
@@ -1206,6 +1285,7 @@ function trackReplaceStep(step, oldState, newTr, attrs) {
|
|
|
1206
1285
|
type: 'insert-slice',
|
|
1207
1286
|
from: adjustedInsertPos,
|
|
1208
1287
|
to: adjustedInsertPos,
|
|
1288
|
+
sliceWasSplit,
|
|
1209
1289
|
slice: new prosemirrorModel.Slice(setFragmentAsInserted(newSliceContent, createNewInsertAttrs(attrs), oldState.schema), openStart, openEnd),
|
|
1210
1290
|
});
|
|
1211
1291
|
}
|
|
@@ -1278,21 +1358,6 @@ function deleteTextIfInserted(node, pos, newTr, schema, deleteAttrs, from, to) {
|
|
|
1278
1358
|
}
|
|
1279
1359
|
}
|
|
1280
1360
|
|
|
1281
|
-
/*!
|
|
1282
|
-
* © 2021 Atypon Systems LLC
|
|
1283
|
-
*
|
|
1284
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
1285
|
-
* you may not use this file except in compliance with the License.
|
|
1286
|
-
* You may obtain a copy of the License at
|
|
1287
|
-
*
|
|
1288
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
1289
|
-
*
|
|
1290
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
1291
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
1292
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
1293
|
-
* See the License for the specific language governing permissions and
|
|
1294
|
-
* limitations under the License.
|
|
1295
|
-
*/
|
|
1296
1361
|
function processChangeSteps(changes, startPos, newTr, emptyAttrs, schema) {
|
|
1297
1362
|
const mapping = new prosemirrorTransform.Mapping();
|
|
1298
1363
|
const deleteAttrs = createNewDeleteAttrs(emptyAttrs);
|
|
@@ -1303,50 +1368,22 @@ function processChangeSteps(changes, startPos, newTr, emptyAttrs, schema) {
|
|
|
1303
1368
|
log.info('process change: ', c);
|
|
1304
1369
|
// const handled = customStepHandler(changes, newTr, emptyAttrs) // ChangeStep[] | undefined
|
|
1305
1370
|
if (c.type === 'delete-node') {
|
|
1306
|
-
|
|
1307
|
-
const
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
const newestStep = newTr.steps[newTr.steps.length - 1];
|
|
1312
|
-
if (step !== newestStep) {
|
|
1313
|
-
mapping.appendMap(newestStep.getMap());
|
|
1314
|
-
step = newestStep;
|
|
1315
|
-
}
|
|
1316
|
-
mergeTrackedMarks(mapping.map(c.pos), newTr.doc, newTr, schema);
|
|
1317
|
-
}
|
|
1318
|
-
else {
|
|
1319
|
-
const attrs = {
|
|
1320
|
-
...c.node.attrs,
|
|
1321
|
-
dataTracked: addTrackIdIfDoesntExist(deleteAttrs),
|
|
1322
|
-
};
|
|
1323
|
-
newTr.setNodeMarkup(mapping.map(c.pos), undefined, attrs, c.node.marks);
|
|
1371
|
+
deleteOrSetNodeDeleted(c.node, mapping.map(c.pos), newTr, deleteAttrs);
|
|
1372
|
+
const newestStep = newTr.steps[newTr.steps.length - 1];
|
|
1373
|
+
if (step !== newestStep) {
|
|
1374
|
+
mapping.appendMap(newestStep.getMap());
|
|
1375
|
+
step = newestStep;
|
|
1324
1376
|
}
|
|
1377
|
+
mergeTrackedMarks(mapping.map(c.pos), newTr.doc, newTr, schema);
|
|
1325
1378
|
}
|
|
1326
1379
|
else if (c.type === 'delete-text') {
|
|
1327
|
-
const from = mapping.map(c.from, -1);
|
|
1328
|
-
const to = mapping.map(c.to, 1);
|
|
1329
1380
|
const node = newTr.doc.nodeAt(mapping.map(c.pos));
|
|
1330
|
-
if (node
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
}
|
|
1334
|
-
else {
|
|
1335
|
-
const leftNode = newTr.doc.resolve(from).nodeBefore;
|
|
1336
|
-
const leftMarks = getMergeableMarkTrackedAttrs(leftNode, deleteAttrs, schema);
|
|
1337
|
-
const rightNode = newTr.doc.resolve(to).nodeAfter;
|
|
1338
|
-
const rightMarks = getMergeableMarkTrackedAttrs(rightNode, deleteAttrs, schema);
|
|
1339
|
-
const fromStartOfMark = from - (leftNode && leftMarks ? leftNode.nodeSize : 0);
|
|
1340
|
-
const toEndOfMark = to + (rightNode && rightMarks ? rightNode.nodeSize : 0);
|
|
1341
|
-
const dataTracked = addTrackIdIfDoesntExist({
|
|
1342
|
-
...leftMarks,
|
|
1343
|
-
...rightMarks,
|
|
1344
|
-
...deleteAttrs,
|
|
1345
|
-
});
|
|
1346
|
-
newTr.addMark(fromStartOfMark, toEndOfMark, schema.marks.tracked_delete.create({
|
|
1347
|
-
dataTracked,
|
|
1348
|
-
}));
|
|
1381
|
+
if (!node) {
|
|
1382
|
+
log.error(`processChangeSteps: no text node found for text-change`, c);
|
|
1383
|
+
return;
|
|
1349
1384
|
}
|
|
1385
|
+
const where = deleteTextIfInserted(node, mapping.map(c.pos), newTr, schema, deleteAttrs, mapping.map(c.from), mapping.map(c.to));
|
|
1386
|
+
mergeTrackedMarks(where, newTr.doc, newTr, schema);
|
|
1350
1387
|
}
|
|
1351
1388
|
else if (c.type === 'merge-fragment') {
|
|
1352
1389
|
let insertPos = mapping.map(c.mergePos);
|
|
@@ -1372,7 +1409,7 @@ function processChangeSteps(changes, startPos, newTr, emptyAttrs, schema) {
|
|
|
1372
1409
|
const newStep = new prosemirrorTransform.ReplaceStep(mapping.map(c.from), mapping.map(c.to), c.slice, false);
|
|
1373
1410
|
const stepResult = newTr.maybeStep(newStep);
|
|
1374
1411
|
if (stepResult.failed) {
|
|
1375
|
-
log.error(`insert ReplaceStep failed
|
|
1412
|
+
log.error(`processChangeSteps: insert-slice ReplaceStep failed "${stepResult.failed}"`, newStep);
|
|
1376
1413
|
return;
|
|
1377
1414
|
}
|
|
1378
1415
|
mergeTrackedMarks(mapping.map(c.from), newTr.doc, newTr, schema);
|
|
@@ -1380,17 +1417,29 @@ function processChangeSteps(changes, startPos, newTr, emptyAttrs, schema) {
|
|
|
1380
1417
|
selectionPos = mapping.map(c.to) + c.slice.size;
|
|
1381
1418
|
}
|
|
1382
1419
|
else if (c.type === 'update-node-attrs') {
|
|
1383
|
-
const oldDataTracked = c.
|
|
1384
|
-
const
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1420
|
+
const oldDataTracked = getBlockInlineTrackedData(c.node) || [];
|
|
1421
|
+
const oldUpdate = oldDataTracked.find((d) => d.operation === exports.CHANGE_OPERATION.set_node_attributes);
|
|
1422
|
+
let newDataTracked = oldDataTracked;
|
|
1423
|
+
if (oldUpdate) {
|
|
1424
|
+
newDataTracked = [
|
|
1425
|
+
...oldDataTracked.filter((d) => d === oldUpdate),
|
|
1426
|
+
{
|
|
1427
|
+
...oldUpdate,
|
|
1428
|
+
updatedAt: emptyAttrs.updatedAt,
|
|
1429
|
+
},
|
|
1430
|
+
];
|
|
1431
|
+
}
|
|
1432
|
+
else if (oldDataTracked.length === 0 ||
|
|
1433
|
+
oldDataTracked.find((d) => d.operation === exports.CHANGE_OPERATION.delete)) {
|
|
1434
|
+
newDataTracked = [
|
|
1435
|
+
...oldDataTracked,
|
|
1436
|
+
addTrackIdIfDoesntExist(createNewUpdateAttrs(emptyAttrs, c.node.attrs)),
|
|
1437
|
+
];
|
|
1438
|
+
}
|
|
1439
|
+
newTr.setNodeMarkup(mapping.map(c.pos), undefined, {
|
|
1440
|
+
...c.newAttrs,
|
|
1441
|
+
dataTracked: newDataTracked.length > 0 ? newDataTracked : null,
|
|
1442
|
+
}, c.node.marks);
|
|
1394
1443
|
}
|
|
1395
1444
|
const newestStep = newTr.steps[newTr.steps.length - 1];
|
|
1396
1445
|
if (step !== newestStep) {
|
|
@@ -1415,25 +1464,26 @@ function processChangeSteps(changes, startPos, newTr, emptyAttrs, schema) {
|
|
|
1415
1464
|
* See the License for the specific language governing permissions and
|
|
1416
1465
|
* limitations under the License.
|
|
1417
1466
|
*/
|
|
1418
|
-
function matchInserted(
|
|
1467
|
+
function matchInserted(matchedDeleted, deleted, inserted, newTr, schema) {
|
|
1419
1468
|
var _a;
|
|
1469
|
+
let matched = [matchedDeleted, deleted];
|
|
1420
1470
|
for (let i = 0;; i += 1) {
|
|
1421
1471
|
if (inserted.childCount === i)
|
|
1422
|
-
return
|
|
1423
|
-
const
|
|
1472
|
+
return matched;
|
|
1473
|
+
const insNode = inserted.child(i);
|
|
1424
1474
|
// @ts-ignore
|
|
1425
|
-
let adjDeleted =
|
|
1426
|
-
(d.type === 'delete-node' && d.
|
|
1427
|
-
if (
|
|
1428
|
-
return
|
|
1475
|
+
let adjDeleted = matched[1].find((d) => (d.type === 'delete-text' && Math.max(d.pos, d.from) === matched[0]) ||
|
|
1476
|
+
(d.type === 'delete-node' && d.pos === matched[0]));
|
|
1477
|
+
if (insNode.type !== ((_a = adjDeleted === null || adjDeleted === void 0 ? void 0 : adjDeleted.node) === null || _a === void 0 ? void 0 : _a.type)) {
|
|
1478
|
+
return matched;
|
|
1429
1479
|
}
|
|
1430
|
-
else if (
|
|
1480
|
+
else if (insNode.isText && (adjDeleted === null || adjDeleted === void 0 ? void 0 : adjDeleted.node)) {
|
|
1431
1481
|
adjDeleted = adjDeleted;
|
|
1432
|
-
const { pos, from, to, node } = adjDeleted;
|
|
1433
|
-
let j = 0, d = from - pos, maxSteps = Math.max(pos, from)
|
|
1482
|
+
const { pos, from, to, node: delNode } = adjDeleted;
|
|
1483
|
+
let j = 0, d = from - pos, maxSteps = to - Math.max(pos, from);
|
|
1434
1484
|
// Match text inside the inserted text node to the deleted text node
|
|
1435
|
-
for (; maxSteps !== j &&
|
|
1436
|
-
|
|
1485
|
+
for (; maxSteps !== j && insNode.text[j] !== undefined && insNode.text[j] === delNode.text[d]; j += 1, d += 1) {
|
|
1486
|
+
matched[0] += 1;
|
|
1437
1487
|
}
|
|
1438
1488
|
// this is needed incase diffing tr.doc
|
|
1439
1489
|
// deleted.push({
|
|
@@ -1443,46 +1493,108 @@ function matchInserted(inDeleted, deleted, inserted, newTr, schema) {
|
|
|
1443
1493
|
// oldAttrs: adjDeleted.node.attrs || {},
|
|
1444
1494
|
// newAttrs: child.attrs || {},
|
|
1445
1495
|
// })
|
|
1496
|
+
matched = [matched[0], matched[1].filter((d) => d !== adjDeleted)];
|
|
1446
1497
|
if (maxSteps !== j) {
|
|
1447
|
-
|
|
1498
|
+
matched[1].push({
|
|
1448
1499
|
pos,
|
|
1449
1500
|
from: Math.max(pos, from) + j,
|
|
1450
1501
|
to,
|
|
1451
1502
|
type: 'delete-text',
|
|
1452
|
-
node,
|
|
1503
|
+
node: delNode,
|
|
1453
1504
|
});
|
|
1505
|
+
return matched;
|
|
1454
1506
|
}
|
|
1455
|
-
|
|
1507
|
+
continue;
|
|
1456
1508
|
}
|
|
1457
|
-
else if (
|
|
1458
|
-
// Move the inDeleted inside the block node's boundary
|
|
1459
|
-
|
|
1509
|
+
else if (insNode.content.size > 0 || (adjDeleted === null || adjDeleted === void 0 ? void 0 : adjDeleted.node.content.size) > 0) {
|
|
1510
|
+
// Move the inDeleted inside the block/inline node's boundary
|
|
1511
|
+
matched = matchInserted(matched[0] + 1, matched[1].filter((d) => d !== adjDeleted), insNode.content);
|
|
1460
1512
|
}
|
|
1461
|
-
|
|
1513
|
+
else {
|
|
1514
|
+
matched = [matched[0] + insNode.nodeSize, matched[1].filter((d) => d !== adjDeleted)];
|
|
1515
|
+
}
|
|
1516
|
+
matched[1].push({
|
|
1462
1517
|
pos: adjDeleted.pos,
|
|
1463
1518
|
type: 'update-node-attrs',
|
|
1519
|
+
node: adjDeleted.node,
|
|
1464
1520
|
// Should check the attrs for equality in fixInconsistentChanges? to remove dataTracked completely
|
|
1465
|
-
|
|
1466
|
-
newAttrs: child.attrs || {},
|
|
1521
|
+
newAttrs: insNode.attrs || {},
|
|
1467
1522
|
});
|
|
1468
|
-
deleted = deleted.filter((d) => d !== adjDeleted);
|
|
1469
|
-
inDeleted -= child.nodeSize;
|
|
1470
1523
|
}
|
|
1471
1524
|
}
|
|
1525
|
+
/**
|
|
1526
|
+
* Cuts a fragment similar to Fragment.cut but also removes the parent node.
|
|
1527
|
+
*
|
|
1528
|
+
* @TODO there is however, some silly calculation mistake so that I need to use matched - deleted + 1 > 0
|
|
1529
|
+
* inside it to check whether to actually cut a text node. The offset might be cascading, therefore it should
|
|
1530
|
+
* be fixed at some point.
|
|
1531
|
+
* @param matched
|
|
1532
|
+
* @param deleted
|
|
1533
|
+
* @param content
|
|
1534
|
+
* @returns
|
|
1535
|
+
*/
|
|
1536
|
+
function cutFragment(matched, deleted, content) {
|
|
1537
|
+
let newContent = [];
|
|
1538
|
+
for (let i = 0; matched <= deleted && i < content.childCount; i += 1) {
|
|
1539
|
+
const child = content.child(i);
|
|
1540
|
+
if (!child.isText && child.content.size > 0) {
|
|
1541
|
+
const cut = cutFragment(matched + 1, deleted, child.content);
|
|
1542
|
+
matched = cut[0];
|
|
1543
|
+
newContent.push(...cut[1].content);
|
|
1544
|
+
}
|
|
1545
|
+
else if (child.isText && matched + child.nodeSize > deleted) {
|
|
1546
|
+
if (matched - deleted + 1 > 0) {
|
|
1547
|
+
newContent.push(child.cut(0, matched - deleted + 1));
|
|
1548
|
+
}
|
|
1549
|
+
else {
|
|
1550
|
+
newContent.push(child);
|
|
1551
|
+
}
|
|
1552
|
+
matched = deleted + 1;
|
|
1553
|
+
}
|
|
1554
|
+
else {
|
|
1555
|
+
matched += child.nodeSize;
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
return [matched, prosemirrorModel.Fragment.fromArray(newContent)];
|
|
1559
|
+
}
|
|
1472
1560
|
function diffChangeSteps(deleted, inserted, newTr, schema) {
|
|
1473
1561
|
const updated = [];
|
|
1474
1562
|
let updatedDeleted = [...deleted];
|
|
1475
1563
|
inserted.forEach((ins) => {
|
|
1476
|
-
|
|
1477
|
-
|
|
1564
|
+
log.info('DIFF ins ', ins);
|
|
1565
|
+
//
|
|
1566
|
+
// @TODO this is a temporary workaround to prevent duplicated diffing between splitSliceIntoMergedParts and
|
|
1567
|
+
// matchInserted.
|
|
1568
|
+
//
|
|
1569
|
+
// As originally authored splitSliceIntoMergedParts splits open slices into their merged parts
|
|
1570
|
+
// leaving out the need to insert the possibly deleted nodes into the doc. However, as matchInserted now
|
|
1571
|
+
// traverses the deleted range checking it against the inserted slice this behaves quite in a same way
|
|
1572
|
+
// where the opened block nodes are traversed but left unmodified. With an openStart > 0 though the
|
|
1573
|
+
// node-attr-updates would additionally have to be filtered out in the processChangeSteps.
|
|
1574
|
+
//
|
|
1575
|
+
// The old logic is still left as it's as refactoring is painful and would probably break something and just
|
|
1576
|
+
// in general, take a lot of time. Therefore, this sliceWasSplit boolean is used to just skip diffing.
|
|
1577
|
+
if (ins.sliceWasSplit) {
|
|
1578
|
+
updated.push(ins);
|
|
1579
|
+
return;
|
|
1580
|
+
}
|
|
1581
|
+
// Start diffing from the start of the deleted range
|
|
1582
|
+
const deleteStart = deleted.reduce((acc, cur) => {
|
|
1583
|
+
if (cur.type === 'delete-node') {
|
|
1584
|
+
return Math.min(acc, cur.pos);
|
|
1585
|
+
}
|
|
1586
|
+
else if (cur.type === 'delete-text') {
|
|
1587
|
+
return Math.min(acc, cur.from);
|
|
1588
|
+
}
|
|
1589
|
+
return acc;
|
|
1590
|
+
}, Number.MAX_SAFE_INTEGER);
|
|
1591
|
+
const [inDeleted, updatedDel] = matchInserted(deleteStart, updatedDeleted, ins.slice.content);
|
|
1592
|
+
if (inDeleted === deleteStart) {
|
|
1478
1593
|
updated.push(ins);
|
|
1479
1594
|
return;
|
|
1480
1595
|
}
|
|
1481
1596
|
updatedDeleted = updatedDel;
|
|
1482
|
-
const
|
|
1483
|
-
const newInsertedB = ins.slice.content.cut(ins.from - inDeleted + 1);
|
|
1484
|
-
// Super hax to cut over block node boundaries in the inserted fragment
|
|
1485
|
-
const newInserted = newInsertedA.size === newInsertedB.size + 2 ? newInsertedB : newInsertedA;
|
|
1597
|
+
const newInserted = cutFragment(0, inDeleted, ins.slice.content)[1];
|
|
1486
1598
|
if (newInserted.size > 0) {
|
|
1487
1599
|
updated.push({
|
|
1488
1600
|
...ins,
|
|
@@ -1490,6 +1602,7 @@ function diffChangeSteps(deleted, inserted, newTr, schema) {
|
|
|
1490
1602
|
});
|
|
1491
1603
|
}
|
|
1492
1604
|
});
|
|
1605
|
+
log.info('FINISH DIFF: ', [...updatedDeleted, ...updated]);
|
|
1493
1606
|
return [...updatedDeleted, ...updated];
|
|
1494
1607
|
}
|
|
1495
1608
|
|
|
@@ -1705,10 +1818,10 @@ const trackChangesPlugin = (opts = { userID: 'anonymous:Anonymous' }) => {
|
|
|
1705
1818
|
ids.forEach((changeId) => {
|
|
1706
1819
|
const change = changeSet === null || changeSet === void 0 ? void 0 : changeSet.get(changeId);
|
|
1707
1820
|
if (change) {
|
|
1708
|
-
createdTr = updateChangeAttrs(createdTr, change, { status, reviewedByID: userID }, oldState.schema);
|
|
1709
|
-
setAction(createdTr, TrackChangesAction.updateChanges, [change.id]);
|
|
1821
|
+
createdTr = updateChangeAttrs(createdTr, change, { ...change.dataTracked, status, reviewedByID: userID }, oldState.schema);
|
|
1710
1822
|
}
|
|
1711
1823
|
});
|
|
1824
|
+
setAction(createdTr, TrackChangesAction.updateChanges, ids);
|
|
1712
1825
|
}
|
|
1713
1826
|
else if (getAction(tr, TrackChangesAction.applyAndRemoveChanges)) {
|
|
1714
1827
|
const mapping = applyAcceptedRejectedChanges(createdTr, oldState.schema, changeSet.nodeChanges);
|