@manuscripts/track-changes-plugin 0.3.0 → 0.4.2

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.
Files changed (29) hide show
  1. package/dist/ChangeSet.d.ts +6 -8
  2. package/dist/{track/steps/track-utils.d.ts → change-steps/diffChangeSteps.d.ts} +4 -3
  3. package/dist/change-steps/matchInserted.d.ts +3 -0
  4. package/dist/change-steps/processChangeSteps.d.ts +21 -0
  5. package/dist/{track → changes}/applyChanges.d.ts +0 -0
  6. package/dist/{track → changes}/findChanges.d.ts +0 -0
  7. package/dist/{track → changes}/fixInconsistentChanges.d.ts +1 -1
  8. package/dist/{track → changes}/updateChangeAttrs.d.ts +0 -0
  9. package/dist/commands.d.ts +1 -1
  10. package/dist/{track/node-utils.d.ts → compute/nodeHelpers.d.ts} +3 -2
  11. package/dist/{track/steps → compute}/setFragmentAsInserted.d.ts +1 -1
  12. package/dist/compute/splitSliceIntoMergedParts.d.ts +41 -0
  13. package/dist/index.cjs +749 -393
  14. package/dist/index.js +749 -393
  15. package/dist/{track/steps → mutate}/deleteAndMergeSplitNodes.d.ts +5 -5
  16. package/dist/{track → mutate}/deleteNode.d.ts +9 -0
  17. package/dist/mutate/deleteText.d.ts +32 -0
  18. package/dist/{track → mutate}/mergeNode.d.ts +0 -0
  19. package/dist/{track/steps → mutate}/mergeTrackedMarks.d.ts +0 -0
  20. package/dist/{track/steps → steps}/trackReplaceAroundStep.d.ts +3 -2
  21. package/dist/{track/steps → steps}/trackReplaceStep.d.ts +3 -2
  22. package/dist/{track → steps}/trackTransaction.d.ts +0 -0
  23. package/dist/types/change.d.ts +23 -15
  24. package/dist/types/step.d.ts +53 -0
  25. package/dist/types/track.d.ts +5 -1
  26. package/dist/utils/track-utils.d.ts +4 -0
  27. package/package.json +3 -2
  28. package/dist/index.es.js +0 -1547
  29. package/dist/types/editor.d.ts +0 -23
package/dist/index.cjs CHANGED
@@ -94,11 +94,11 @@ exports.CHANGE_OPERATION = void 0;
94
94
  (function (CHANGE_OPERATION) {
95
95
  CHANGE_OPERATION["insert"] = "insert";
96
96
  CHANGE_OPERATION["delete"] = "delete";
97
- CHANGE_OPERATION["set_node_attributes"] = "set_node_attributes";
98
- CHANGE_OPERATION["wrap_with_node"] = "wrap_with_node";
99
- CHANGE_OPERATION["unwrap_from_node"] = "unwrap_from_node";
100
- CHANGE_OPERATION["add_mark"] = "add_mark";
101
- CHANGE_OPERATION["remove_mark"] = "remove_mark";
97
+ CHANGE_OPERATION["set_node_attributes"] = "set_attrs";
98
+ // wrap_with_node = 'wrap_with_node',
99
+ // unwrap_from_node = 'unwrap_from_node',
100
+ // add_mark = 'add_mark',
101
+ // remove_mark = 'remove_mark',
102
102
  })(exports.CHANGE_OPERATION || (exports.CHANGE_OPERATION = {}));
103
103
  exports.CHANGE_STATUS = void 0;
104
104
  (function (CHANGE_STATUS) {
@@ -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.attrs.id) && ChangeSet.isValidTrackedAttrs(c.attrs);
183
- iteratedIds.add(c.attrs.id);
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
  }
@@ -199,16 +199,13 @@ class ChangeSet {
199
199
  rootNodes.push(currentNodeChange);
200
200
  currentNodeChange = undefined;
201
201
  }
202
- if (c.type === 'node-change' && currentNodeChange && c.from < currentNodeChange.to) {
202
+ if (currentNodeChange && c.from < currentNodeChange.to) {
203
203
  currentNodeChange.children.push(c);
204
204
  }
205
205
  else if (c.type === 'node-change') {
206
206
  currentNodeChange = { ...c, children: [] };
207
207
  }
208
- else if (c.type === 'text-change' && currentNodeChange && c.from < currentNodeChange.to) {
209
- currentNodeChange.children.push(c);
210
- }
211
- else if (c.type === 'text-change') {
208
+ else {
212
209
  rootNodes.push(c);
213
210
  }
214
211
  });
@@ -218,13 +215,13 @@ class ChangeSet {
218
215
  return rootNodes;
219
216
  }
220
217
  get pending() {
221
- return this.changeTree.filter((c) => c.attrs.status === exports.CHANGE_STATUS.pending);
218
+ return this.changeTree.filter((c) => c.dataTracked.status === exports.CHANGE_STATUS.pending);
222
219
  }
223
220
  get accepted() {
224
- return this.changeTree.filter((c) => c.attrs.status === exports.CHANGE_STATUS.accepted);
221
+ return this.changeTree.filter((c) => c.dataTracked.status === exports.CHANGE_STATUS.accepted);
225
222
  }
226
223
  get rejected() {
227
- return this.changeTree.filter((c) => c.attrs.status === exports.CHANGE_STATUS.rejected);
224
+ return this.changeTree.filter((c) => c.dataTracked.status === exports.CHANGE_STATUS.rejected);
228
225
  }
229
226
  get textChanges() {
230
227
  return this.changes.filter((c) => c.type === 'text-change');
@@ -232,6 +229,12 @@ class ChangeSet {
232
229
  get nodeChanges() {
233
230
  return this.changes.filter((c) => c.type === 'node-change');
234
231
  }
232
+ get nodeAttrChanges() {
233
+ return this.changes.filter((c) => c.type === 'node-attr-change');
234
+ }
235
+ get bothNodeChanges() {
236
+ return this.changes.filter((c) => c.type === 'node-change' || c.type === 'node-attr-change');
237
+ }
235
238
  get isEmpty() {
236
239
  return __classPrivateFieldGet(this, _ChangeSet_changes, "f").length === 0;
237
240
  }
@@ -252,7 +255,7 @@ class ChangeSet {
252
255
  });
253
256
  }
254
257
  get hasIncompleteAttrs() {
255
- return __classPrivateFieldGet(this, _ChangeSet_changes, "f").some((c) => !ChangeSet.isValidTrackedAttrs(c.attrs));
258
+ return __classPrivateFieldGet(this, _ChangeSet_changes, "f").some((c) => !ChangeSet.isValidDataTracked(c.dataTracked));
256
259
  }
257
260
  get(id) {
258
261
  return __classPrivateFieldGet(this, _ChangeSet_changes, "f").find((c) => c.id === id);
@@ -272,31 +275,22 @@ class ChangeSet {
272
275
  static flattenTreeToIds(changes) {
273
276
  return changes.flatMap((c) => this.isNodeChange(c) ? [c.id, ...c.children.map((c) => c.id)] : c.id);
274
277
  }
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
- const { status, operation } = change.attrs;
281
- return ((operation === exports.CHANGE_OPERATION.insert && status === exports.CHANGE_STATUS.accepted) ||
282
- (operation === exports.CHANGE_OPERATION.delete && status === exports.CHANGE_STATUS.rejected));
283
- }
284
278
  /**
285
279
  * Determines whether a change should be deleted when applying it to the document.
286
280
  * @param change
287
281
  */
288
282
  static shouldDeleteChange(change) {
289
- const { status, operation } = change.attrs;
283
+ const { status, operation } = change.dataTracked;
290
284
  return ((operation === exports.CHANGE_OPERATION.insert && status === exports.CHANGE_STATUS.rejected) ||
291
285
  (operation === exports.CHANGE_OPERATION.delete && status === exports.CHANGE_STATUS.accepted));
292
286
  }
293
287
  /**
294
288
  * Checks whether change attributes contain all TrackedAttrs keys with non-undefined values
295
- * @param attrs
289
+ * @param dataTracked
296
290
  */
297
- static isValidTrackedAttrs(attrs = {}) {
298
- if ('attrs' in attrs) {
299
- log.warn('passed "attrs" as property to isValidTrackedAttrs(attrs)', attrs);
291
+ static isValidDataTracked(dataTracked = {}) {
292
+ if ('dataTracked' in dataTracked) {
293
+ log.warn('passed "dataTracked" as property to isValidTrackedAttrs()', dataTracked);
300
294
  }
301
295
  const trackedKeys = [
302
296
  'id',
@@ -307,14 +301,14 @@ class ChangeSet {
307
301
  'updatedAt',
308
302
  ];
309
303
  // reviewedByID is set optional since either ProseMirror or Yjs doesn't like persisting null values inside attributes objects
310
- // So it can be either omitted completely or at least null or string
304
+ // So it can be either omitted completely or at least be null or string
311
305
  const optionalKeys = ['reviewedByID'];
312
- const entries = Object.entries(attrs).filter(([key, val]) => trackedKeys.includes(key));
313
- const optionalEntries = Object.entries(attrs).filter(([key, val]) => optionalKeys.includes(key));
306
+ const entries = Object.entries(dataTracked).filter(([key, val]) => trackedKeys.includes(key));
307
+ const optionalEntries = Object.entries(dataTracked).filter(([key, val]) => optionalKeys.includes(key));
314
308
  return (entries.length === trackedKeys.length &&
315
309
  entries.every(([key, val]) => trackedKeys.includes(key) && val !== undefined) &&
316
310
  optionalEntries.every(([key, val]) => optionalKeys.includes(key) && val !== undefined) &&
317
- (attrs.id || '').length > 0 // Changes created with undefined id have '' as placeholder
311
+ (dataTracked.id || '').length > 0 // Changes created with undefined id have '' as placeholder
318
312
  );
319
313
  }
320
314
  static isTextChange(change) {
@@ -323,9 +317,102 @@ class ChangeSet {
323
317
  static isNodeChange(change) {
324
318
  return change.type === 'node-change';
325
319
  }
320
+ static isNodeAttrChange(change) {
321
+ return change.type === 'node-attr-change';
322
+ }
326
323
  }
327
324
  _ChangeSet_changes = new WeakMap();
328
325
 
326
+ /*!
327
+ * © 2021 Atypon Systems LLC
328
+ *
329
+ * Licensed under the Apache License, Version 2.0 (the "License");
330
+ * you may not use this file except in compliance with the License.
331
+ * You may obtain a copy of the License at
332
+ *
333
+ * http://www.apache.org/licenses/LICENSE-2.0
334
+ *
335
+ * Unless required by applicable law or agreed to in writing, software
336
+ * distributed under the License is distributed on an "AS IS" BASIS,
337
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
338
+ * See the License for the specific language governing permissions and
339
+ * limitations under the License.
340
+ */
341
+ function uuidv4() {
342
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
343
+ const r = (Math.random() * 16) | 0, v = c == 'x' ? r : (r & 0x3) | 0x8;
344
+ return v.toString(16);
345
+ });
346
+ }
347
+
348
+ function addTrackIdIfDoesntExist(attrs) {
349
+ if (!attrs.id) {
350
+ return {
351
+ id: uuidv4(),
352
+ ...attrs,
353
+ };
354
+ }
355
+ return attrs;
356
+ }
357
+ function getTextNodeTrackedMarkData(node, schema) {
358
+ if (!node || !node.isText) {
359
+ return undefined;
360
+ }
361
+ const marksTrackedData = [];
362
+ node.marks.forEach((mark) => {
363
+ if (mark.type === schema.marks.tracked_insert || mark.type === schema.marks.tracked_delete) {
364
+ const operation = mark.type === schema.marks.tracked_insert
365
+ ? exports.CHANGE_OPERATION.insert
366
+ : exports.CHANGE_OPERATION.delete;
367
+ marksTrackedData.push({ ...mark.attrs.dataTracked, operation });
368
+ }
369
+ });
370
+ if (marksTrackedData.length > 1) {
371
+ log.warn('inline node with more than 1 of tracked marks', marksTrackedData);
372
+ }
373
+ return marksTrackedData[0] || undefined;
374
+ }
375
+ function getBlockInlineTrackedData(node) {
376
+ let { dataTracked } = node.attrs;
377
+ if (dataTracked && !Array.isArray(dataTracked)) {
378
+ return [dataTracked];
379
+ }
380
+ return dataTracked;
381
+ }
382
+ function getNodeTrackedData(node, schema) {
383
+ let tracked;
384
+ if (node && !node.isText) {
385
+ tracked = getBlockInlineTrackedData(node);
386
+ }
387
+ else if (node === null || node === void 0 ? void 0 : node.isText) {
388
+ tracked = getTextNodeTrackedMarkData(node, schema);
389
+ }
390
+ if (tracked && !Array.isArray(tracked)) {
391
+ tracked = [tracked];
392
+ }
393
+ return tracked;
394
+ }
395
+ function equalMarks(n1, n2) {
396
+ return (n1.marks.length === n2.marks.length &&
397
+ n1.marks.every((mark) => n1.marks.find((m) => m.type === mark.type)));
398
+ }
399
+ function shouldMergeTrackedAttributes(left, right) {
400
+ if (!left || !right) {
401
+ log.warn('passed undefined dataTracked attributes to shouldMergeTrackedAttributes', {
402
+ left,
403
+ right,
404
+ });
405
+ return false;
406
+ }
407
+ return (left.status === right.status &&
408
+ left.operation === right.operation &&
409
+ left.authorID === right.authorID);
410
+ }
411
+ function getMergeableMarkTrackedAttrs(node, attrs, schema) {
412
+ const nodeAttrs = getTextNodeTrackedMarkData(node, schema);
413
+ return nodeAttrs && shouldMergeTrackedAttributes(nodeAttrs, attrs) ? nodeAttrs : null;
414
+ }
415
+
329
416
  /*!
330
417
  * © 2021 Atypon Systems LLC
331
418
  *
@@ -371,6 +458,29 @@ function deleteNode(node, pos, tr) {
371
458
  // child, say some wrapper blockNode, is also deleted the content could be retained. TODO I guess.
372
459
  return tr.delete(pos, pos + node.nodeSize);
373
460
  }
461
+ }
462
+ /**
463
+ * Deletes inserted block or inline node, otherwise adds `dataTracked` object with CHANGE_STATUS 'deleted'
464
+ * @param node
465
+ * @param pos
466
+ * @param newTr
467
+ * @param deleteAttrs
468
+ */
469
+ function deleteOrSetNodeDeleted(node, pos, newTr, deleteAttrs) {
470
+ const dataTracked = getBlockInlineTrackedData(node);
471
+ const inserted = dataTracked === null || dataTracked === void 0 ? void 0 : dataTracked.find((d) => d.operation === exports.CHANGE_OPERATION.insert);
472
+ const deleted = dataTracked === null || dataTracked === void 0 ? void 0 : dataTracked.find((d) => d.operation === exports.CHANGE_OPERATION.delete);
473
+ const updated = dataTracked === null || dataTracked === void 0 ? void 0 : dataTracked.find((d) => d.operation === exports.CHANGE_OPERATION.set_node_attributes);
474
+ if (inserted && inserted.authorID === deleteAttrs.authorID) {
475
+ return deleteNode(node, pos, newTr);
476
+ }
477
+ const newDeleted = deleted
478
+ ? { ...deleted, updatedAt: deleteAttrs.updatedAt }
479
+ : addTrackIdIfDoesntExist(deleteAttrs);
480
+ newTr.setNodeMarkup(pos, undefined, {
481
+ ...node.attrs,
482
+ dataTracked: updated ? [newDeleted, updated] : [newDeleted],
483
+ }, node.marks);
374
484
  }
375
485
 
376
486
  /*!
@@ -413,101 +523,49 @@ function mergeNode(node, pos, tr) {
413
523
  return undefined;
414
524
  }
415
525
 
416
- /*!
417
- * © 2021 Atypon Systems LLC
418
- *
419
- * Licensed under the Apache License, Version 2.0 (the "License");
420
- * you may not use this file except in compliance with the License.
421
- * You may obtain a copy of the License at
422
- *
423
- * http://www.apache.org/licenses/LICENSE-2.0
424
- *
425
- * Unless required by applicable law or agreed to in writing, software
426
- * distributed under the License is distributed on an "AS IS" BASIS,
427
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
428
- * See the License for the specific language governing permissions and
429
- * limitations under the License.
430
- */
431
- function uuidv4() {
432
- return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
433
- const r = (Math.random() * 16) | 0, v = c == 'x' ? r : (r & 0x3) | 0x8;
434
- return v.toString(16);
435
- });
436
- }
437
-
438
- function addTrackIdIfDoesntExist(attrs) {
439
- if (!attrs.id) {
440
- return {
441
- id: uuidv4(),
442
- ...attrs,
443
- };
444
- }
445
- return attrs;
446
- }
447
- function getInlineNodeTrackedMarkData(node, schema) {
448
- if (!node || !node.isInline) {
449
- return undefined;
450
- }
451
- const marksTrackedData = [];
452
- node.marks.forEach((mark) => {
453
- if (mark.type === schema.marks.tracked_insert || mark.type === schema.marks.tracked_delete) {
454
- const operation = mark.type === schema.marks.tracked_insert
455
- ? exports.CHANGE_OPERATION.insert
456
- : exports.CHANGE_OPERATION.delete;
457
- marksTrackedData.push({ ...mark.attrs.dataTracked, operation });
458
- }
459
- });
460
- if (marksTrackedData.length > 1) {
461
- log.warn('inline node with more than 1 of tracked marks', marksTrackedData);
462
- }
463
- return marksTrackedData[0] || undefined;
464
- }
465
- function getNodeTrackedData(node, schema) {
466
- return !node
467
- ? undefined
468
- : node.isText
469
- ? getInlineNodeTrackedMarkData(node, schema)
470
- : node.attrs.dataTracked;
471
- }
472
- function equalMarks(n1, n2) {
473
- return (n1.marks.length === n2.marks.length &&
474
- n1.marks.every((mark) => n1.marks.find((m) => m.type === mark.type)));
475
- }
476
- function shouldMergeTrackedAttributes(left, right) {
477
- if (!left || !right) {
478
- log.warn('passed undefined dataTracked attributes to shouldMergeTrackedAttributes', {
479
- left,
480
- right,
481
- });
482
- return false;
483
- }
484
- return (left.status === right.status &&
485
- left.operation === right.operation &&
486
- left.authorID === right.authorID);
487
- }
488
- function getMergeableMarkTrackedAttrs(node, attrs, schema) {
489
- const nodeAttrs = getInlineNodeTrackedMarkData(node, schema);
490
- return nodeAttrs && shouldMergeTrackedAttributes(nodeAttrs, attrs) ? nodeAttrs : null;
491
- }
492
-
493
526
  function updateChangeAttrs(tr, change, trackedAttrs, schema) {
494
527
  const node = tr.doc.nodeAt(change.from);
495
528
  if (!node) {
496
- throw Error('No node at the from of change' + change);
529
+ log.error('updateChangeAttrs: no node at the from of change ', change);
530
+ return tr;
531
+ }
532
+ const { operation } = trackedAttrs;
533
+ const oldTrackData = change.type === 'text-change'
534
+ ? getTextNodeTrackedMarkData(node, schema)
535
+ : getBlockInlineTrackedData(node);
536
+ if (!operation) {
537
+ log.warn('updateChangeAttrs: unable to determine operation of change ', change);
538
+ }
539
+ else if (!oldTrackData) {
540
+ log.warn('updateChangeAttrs: no old dataTracked for change ', change);
541
+ }
542
+ if (change.type === 'text-change') {
543
+ const oldMark = node.marks.find((m) => m.type === schema.marks.tracked_insert || m.type === schema.marks.tracked_delete);
544
+ if (!oldMark) {
545
+ log.warn('updateChangeAttrs: no track marks for a text-change ', change);
546
+ return tr;
547
+ }
548
+ // TODO add operation based on mark type if it's undefined?
549
+ tr.addMark(change.from, change.to, oldMark.type.create({ ...oldMark.attrs, dataTracked: { ...oldTrackData, ...trackedAttrs } }));
497
550
  }
498
- const dataTracked = { ...getNodeTrackedData(node, schema), ...trackedAttrs };
499
- const oldMark = node.marks.find((m) => m.type === schema.marks.tracked_insert || m.type === schema.marks.tracked_delete);
500
- if (change.type === 'text-change' && oldMark) {
501
- tr.addMark(change.from, change.to, oldMark.type.create({ ...oldMark.attrs, dataTracked }));
551
+ else if ((change.type === 'node-change' || change.type === 'node-attr-change') && !operation) {
552
+ // Very weird edge-case if this happens
553
+ tr.setNodeMarkup(change.from, undefined, { ...node.attrs, dataTracked: null }, node.marks);
502
554
  }
503
- else if (change.type === 'node-change') {
504
- tr.setNodeMarkup(change.from, undefined, { ...node.attrs, dataTracked }, node.marks);
555
+ else if (change.type === 'node-change' || change.type === 'node-attr-change') {
556
+ const newDataTracked = (getBlockInlineTrackedData(node) || []).map((oldTrack) => {
557
+ if (oldTrack.operation === operation) {
558
+ return { ...oldTrack, ...trackedAttrs };
559
+ }
560
+ return oldTrack;
561
+ });
562
+ tr.setNodeMarkup(change.from, undefined, { ...node.attrs, dataTracked: newDataTracked.length === 0 ? null : newDataTracked }, node.marks);
505
563
  }
506
564
  return tr;
507
565
  }
508
566
  function updateChangeChildrenAttributes(changes, tr, mapping) {
509
567
  changes.forEach((c) => {
510
- if (c.type === 'node-change' && ChangeSet.shouldNotDelete(c)) {
568
+ if (c.type === 'node-change' && !ChangeSet.shouldDeleteChange(c)) {
511
569
  const from = mapping.map(c.from);
512
570
  const node = tr.doc.nodeAt(from);
513
571
  if (!node) {
@@ -529,12 +587,12 @@ function updateChangeChildrenAttributes(changes, tr, mapping) {
529
587
  */
530
588
  function applyAcceptedRejectedChanges(tr, schema, changes, deleteMap = new prosemirrorTransform.Mapping()) {
531
589
  changes.forEach((change) => {
532
- if (change.attrs.status === exports.CHANGE_STATUS.pending) {
590
+ if (change.dataTracked.status === exports.CHANGE_STATUS.pending) {
533
591
  return;
534
592
  }
535
593
  // Map change.from and skip those which dont need to be applied
536
594
  // or were already deleted by an applied block delete
537
- const { pos: from, deleted } = deleteMap.mapResult(change.from), node = tr.doc.nodeAt(from), noChangeNeeded = deleted || ChangeSet.shouldNotDelete(change);
595
+ const { pos: from, deleted } = deleteMap.mapResult(change.from), node = tr.doc.nodeAt(from), noChangeNeeded = deleted || !ChangeSet.shouldDeleteChange(change);
538
596
  if (!node) {
539
597
  !deleted && log.warn('no node found to update for change', change);
540
598
  return;
@@ -562,6 +620,14 @@ function applyAcceptedRejectedChanges(tr, schema, changes, deleteMap = new prose
562
620
  }
563
621
  deleteMap.appendMap(tr.steps[tr.steps.length - 1].getMap());
564
622
  }
623
+ else if (ChangeSet.isNodeAttrChange(change) &&
624
+ change.dataTracked.status === exports.CHANGE_STATUS.accepted) {
625
+ tr.setNodeMarkup(from, undefined, { ...node.attrs, dataTracked: null }, node.marks);
626
+ }
627
+ else if (ChangeSet.isNodeAttrChange(change) &&
628
+ change.dataTracked.status === exports.CHANGE_STATUS.rejected) {
629
+ tr.setNodeMarkup(from, undefined, { ...change.oldAttrs, dataTracked: null }, node.marks);
630
+ }
565
631
  });
566
632
  return deleteMap;
567
633
  }
@@ -580,9 +646,10 @@ function findChanges(state) {
580
646
  // Store the last iterated change to join adjacent text changes
581
647
  let current;
582
648
  state.doc.descendants((node, pos) => {
583
- const attrs = getNodeTrackedData(node, state.schema);
584
- if (attrs) {
585
- const id = (attrs === null || attrs === void 0 ? void 0 : attrs.id) || '';
649
+ const tracked = getNodeTrackedData(node, state.schema) || [];
650
+ for (let i = 0; i < tracked.length; i += 1) {
651
+ const dataTracked = tracked[i];
652
+ const id = dataTracked.id || '';
586
653
  // Join adjacent text changes that have been broken up due to different marks
587
654
  // eg <ins><b>bold</b>norm<i>italic</i></ins> -> treated as one continuous change
588
655
  // Note the !equalMarks to leave changes separate incase the marks are equal to let fixInconsistentChanges to fix them
@@ -594,38 +661,49 @@ function findChanges(state) {
594
661
  current.change.to = pos + node.nodeSize;
595
662
  // Important to update the node as the text changes might contain multiple parts where some marks equal each other
596
663
  current.node = node;
664
+ continue;
597
665
  }
598
- else if (node.isText) {
599
- current && changes.push(current.change);
600
- current = {
601
- change: {
602
- id,
603
- type: 'text-change',
604
- from: pos,
605
- to: pos + node.nodeSize,
606
- text: node.text,
607
- attrs,
608
- },
609
- node,
666
+ current && changes.push(current.change);
667
+ let change;
668
+ if (node.isText) {
669
+ change = {
670
+ id,
671
+ type: 'text-change',
672
+ from: pos,
673
+ to: pos + node.nodeSize,
674
+ dataTracked,
675
+ text: node.text,
676
+ };
677
+ }
678
+ else if (dataTracked.operation === exports.CHANGE_OPERATION.set_node_attributes) {
679
+ change = {
680
+ id,
681
+ type: 'node-attr-change',
682
+ from: pos,
683
+ to: pos + node.nodeSize,
684
+ dataTracked,
685
+ nodeType: node.type.name,
686
+ newAttrs: node.attrs,
687
+ oldAttrs: dataTracked.oldAttrs,
610
688
  };
611
689
  }
612
690
  else {
613
- current && changes.push(current.change);
614
- current = {
615
- change: {
616
- id,
617
- type: 'node-change',
618
- from: pos,
619
- to: pos + node.nodeSize,
620
- nodeType: node.type.name,
621
- children: [],
622
- attrs,
623
- },
624
- node,
691
+ change = {
692
+ id,
693
+ type: 'node-change',
694
+ from: pos,
695
+ to: pos + node.nodeSize,
696
+ dataTracked,
697
+ nodeType: node.type.name,
698
+ children: [],
625
699
  };
626
700
  }
701
+ current = {
702
+ change,
703
+ node,
704
+ };
627
705
  }
628
- else if (current) {
706
+ if (tracked.length === 0 && current) {
629
707
  changes.push(current.change);
630
708
  current = undefined;
631
709
  }
@@ -644,22 +722,23 @@ function findChanges(state) {
644
722
  * @param schema
645
723
  * @return docWasChanged, a boolean
646
724
  */
647
- function fixInconsistentChanges(changeSet, trackUserID, newTr, schema) {
725
+ function fixInconsistentChanges(changeSet, currentUserID, newTr, schema) {
648
726
  const iteratedIds = new Set();
649
727
  let changed = false;
650
728
  changeSet.invalidChanges.forEach((c) => {
651
- const { id, authorID, operation, reviewedByID, status, createdAt, updatedAt } = c.attrs;
729
+ const { id, authorID, operation, reviewedByID, status, createdAt, updatedAt } = c.dataTracked;
652
730
  const newAttrs = {
653
731
  ...((!id || iteratedIds.has(id) || id.length === 0) && { id: uuidv4() }),
654
- ...(!authorID && { authorID: trackUserID }),
655
- ...(!operation && { operation: exports.CHANGE_OPERATION.insert }),
732
+ ...(!authorID && { authorID: currentUserID }),
733
+ // Dont add a default operation -> rather have updateChangeAttrs delete the track data
734
+ // ...(!operation && { operation: CHANGE_OPERATION.insert }),
656
735
  ...(!reviewedByID && { reviewedByID: null }),
657
736
  ...(!status && { status: exports.CHANGE_STATUS.pending }),
658
737
  ...(!createdAt && { createdAt: Date.now() }),
659
738
  ...(!updatedAt && { updatedAt: Date.now() }),
660
739
  };
661
740
  if (Object.keys(newAttrs).length > 0) {
662
- updateChangeAttrs(newTr, c, { ...c.attrs, ...newAttrs }, schema);
741
+ updateChangeAttrs(newTr, c, { ...c.dataTracked, ...newAttrs }, schema);
663
742
  changed = true;
664
743
  }
665
744
  iteratedIds.add(newAttrs.id || id);
@@ -667,87 +746,6 @@ function fixInconsistentChanges(changeSet, trackUserID, newTr, schema) {
667
746
  return changed;
668
747
  }
669
748
 
670
- /*!
671
- * © 2021 Atypon Systems LLC
672
- *
673
- * Licensed under the Apache License, Version 2.0 (the "License");
674
- * you may not use this file except in compliance with the License.
675
- * You may obtain a copy of the License at
676
- *
677
- * http://www.apache.org/licenses/LICENSE-2.0
678
- *
679
- * Unless required by applicable law or agreed to in writing, software
680
- * distributed under the License is distributed on an "AS IS" BASIS,
681
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
682
- * See the License for the specific language governing permissions and
683
- * limitations under the License.
684
- */
685
- function markInlineNodeChange(node, newTrackAttrs, schema) {
686
- const filtered = node.marks.filter((m) => m.type !== schema.marks.tracked_insert && m.type !== schema.marks.tracked_delete);
687
- const mark = newTrackAttrs.operation === exports.CHANGE_OPERATION.insert
688
- ? schema.marks.tracked_insert
689
- : schema.marks.tracked_delete;
690
- const createdMark = mark.create({
691
- dataTracked: addTrackIdIfDoesntExist(newTrackAttrs),
692
- });
693
- return node.mark(filtered.concat(createdMark));
694
- }
695
- function recurseNodeContent(node, newTrackAttrs, schema) {
696
- if (node.isText) {
697
- return markInlineNodeChange(node, newTrackAttrs, schema);
698
- }
699
- else if (node.isBlock || node.isInline) {
700
- const updatedChildren = [];
701
- node.content.forEach((child) => {
702
- updatedChildren.push(recurseNodeContent(child, newTrackAttrs, schema));
703
- });
704
- return node.type.create({
705
- ...node.attrs,
706
- dataTracked: addTrackIdIfDoesntExist(newTrackAttrs),
707
- }, prosemirrorModel.Fragment.fromArray(updatedChildren), node.marks);
708
- }
709
- else {
710
- log.error(`unhandled node type: "${node.type.name}"`, node);
711
- return node;
712
- }
713
- }
714
- function setFragmentAsInserted(inserted, insertAttrs, schema) {
715
- // Recurse the content in the inserted slice and either mark it tracked_insert or set node attrs
716
- const updatedInserted = [];
717
- inserted.forEach((n) => {
718
- updatedInserted.push(recurseNodeContent(n, insertAttrs, schema));
719
- });
720
- return updatedInserted.length === 0 ? inserted : prosemirrorModel.Fragment.fromArray(updatedInserted);
721
- }
722
-
723
- /*!
724
- * © 2021 Atypon Systems LLC
725
- *
726
- * Licensed under the Apache License, Version 2.0 (the "License");
727
- * you may not use this file except in compliance with the License.
728
- * You may obtain a copy of the License at
729
- *
730
- * http://www.apache.org/licenses/LICENSE-2.0
731
- *
732
- * Unless required by applicable law or agreed to in writing, software
733
- * distributed under the License is distributed on an "AS IS" BASIS,
734
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
735
- * See the License for the specific language governing permissions and
736
- * limitations under the License.
737
- */
738
- function createNewInsertAttrs(attrs) {
739
- return {
740
- ...attrs,
741
- operation: exports.CHANGE_OPERATION.insert,
742
- };
743
- }
744
- function createNewDeleteAttrs(attrs) {
745
- return {
746
- ...attrs,
747
- operation: exports.CHANGE_OPERATION.delete,
748
- };
749
- }
750
-
751
749
  /*!
752
750
  * © 2021 Atypon Systems LLC
753
751
  *
@@ -843,73 +841,135 @@ function splitSliceIntoMergedParts(insertSlice, mergeEqualSides = false) {
843
841
  firstMergedNode,
844
842
  lastMergedNode,
845
843
  };
846
- }
847
- /**
848
- * Deletes inserted text directly, otherwise wraps it with tracked_delete mark
844
+ }
845
+
846
+ /*!
847
+ * © 2021 Atypon Systems LLC
849
848
  *
850
- * This would work for general inline nodes too, but since node marks don't work properly
851
- * with Yjs, attributes are used instead.
852
- * @param node
853
- * @param pos
854
- * @param newTr
855
- * @param schema
856
- * @param deleteAttrs
857
- * @param from
858
- * @param to
849
+ * Licensed under the Apache License, Version 2.0 (the "License");
850
+ * you may not use this file except in compliance with the License.
851
+ * You may obtain a copy of the License at
852
+ *
853
+ * http://www.apache.org/licenses/LICENSE-2.0
854
+ *
855
+ * Unless required by applicable law or agreed to in writing, software
856
+ * distributed under the License is distributed on an "AS IS" BASIS,
857
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
858
+ * See the License for the specific language governing permissions and
859
+ * limitations under the License.
859
860
  */
860
- function deleteTextIfInserted(node, pos, newTr, schema, deleteAttrs, from, to) {
861
- const start = from ? Math.max(pos, from) : pos;
862
- const nodeEnd = pos + node.nodeSize;
863
- const end = to ? Math.min(nodeEnd, to) : nodeEnd;
864
- if (node.marks.find((m) => m.type === schema.marks.tracked_insert)) {
865
- // Math.max(pos, from) is for picking always the start of the node,
866
- // not the start of the change (which might span multiple nodes).
867
- // Pos can be less than from as nodesBetween iterates through all nodes starting from the top block node
868
- newTr.replaceWith(start, end, prosemirrorModel.Fragment.empty);
869
- return start;
870
- }
871
- else {
872
- const leftNode = newTr.doc.resolve(start).nodeBefore;
873
- const leftMarks = getMergeableMarkTrackedAttrs(leftNode, deleteAttrs, schema);
874
- const rightNode = newTr.doc.resolve(end).nodeAfter;
875
- const rightMarks = getMergeableMarkTrackedAttrs(rightNode, deleteAttrs, schema);
876
- const fromStartOfMark = start - (leftNode && leftMarks ? leftNode.nodeSize : 0);
877
- const toEndOfMark = end + (rightNode && rightMarks ? rightNode.nodeSize : 0);
878
- const createdAt = Math.min((leftMarks === null || leftMarks === void 0 ? void 0 : leftMarks.createdAt) || Number.MAX_VALUE, (rightMarks === null || rightMarks === void 0 ? void 0 : rightMarks.createdAt) || Number.MAX_VALUE, deleteAttrs.createdAt);
879
- const dataTracked = addTrackIdIfDoesntExist({
880
- ...leftMarks,
881
- ...rightMarks,
882
- ...deleteAttrs,
883
- createdAt,
884
- });
885
- newTr.addMark(fromStartOfMark, toEndOfMark, schema.marks.tracked_delete.create({
886
- dataTracked,
887
- }));
888
- return toEndOfMark;
889
- }
861
+ function markInlineNodeChange(node, newTrackAttrs, schema) {
862
+ const filtered = node.marks.filter((m) => m.type !== schema.marks.tracked_insert && m.type !== schema.marks.tracked_delete);
863
+ const mark = newTrackAttrs.operation === exports.CHANGE_OPERATION.insert
864
+ ? schema.marks.tracked_insert
865
+ : schema.marks.tracked_delete;
866
+ const createdMark = mark.create({
867
+ dataTracked: addTrackIdIfDoesntExist(newTrackAttrs),
868
+ });
869
+ return node.mark(filtered.concat(createdMark));
890
870
  }
891
871
  /**
892
- * Deletes inserted block or inline node, otherwise adds `dataTracked` object with CHANGE_STATUS 'deleted'
893
- * @param node
894
- * @param pos
895
- * @param newTr
896
- * @param deleteAttrs
872
+ * Iterates over fragment's content and joins pasted text with old track marks
873
+ *
874
+ * This is not strictly necessary but it's kinda bad UX if the inserted text is split into parts
875
+ * even when it's authored by the same user.
876
+ * @param content
877
+ * @param newTrackAttrs
878
+ * @param schema
879
+ * @returns
897
880
  */
898
- function deleteOrSetNodeDeleted(node, pos, newTr, deleteAttrs) {
899
- const dataTracked = node.attrs.dataTracked;
900
- const wasInsertedBySameUser = (dataTracked === null || dataTracked === void 0 ? void 0 : dataTracked.operation) === exports.CHANGE_OPERATION.insert &&
901
- dataTracked.authorID === deleteAttrs.authorID;
902
- if (wasInsertedBySameUser) {
903
- deleteNode(node, pos, newTr);
881
+ function loopContentAndMergeText(content, newTrackAttrs, schema) {
882
+ var _a;
883
+ const updatedChildren = [];
884
+ for (let i = 0; i < content.childCount; i += 1) {
885
+ const recursed = recurseNodeContent(content.child(i), newTrackAttrs, schema);
886
+ const prev = i > 0 ? updatedChildren[i - 1] : null;
887
+ if ((prev === null || prev === void 0 ? void 0 : prev.isText) &&
888
+ recursed.isText &&
889
+ equalMarks(prev, recursed) &&
890
+ ((_a = getTextNodeTrackedMarkData(prev, schema)) === null || _a === void 0 ? void 0 : _a.operation) === exports.CHANGE_OPERATION.insert) {
891
+ updatedChildren.splice(i - 1, 1, schema.text('' + prev.text + recursed.text, prev.marks));
892
+ }
893
+ else {
894
+ updatedChildren.push(recursed);
895
+ }
904
896
  }
905
- else {
906
- const attrs = {
897
+ return updatedChildren;
898
+ }
899
+ function recurseNodeContent(node, newTrackAttrs, schema) {
900
+ if (node.isText) {
901
+ return markInlineNodeChange(node, newTrackAttrs, schema);
902
+ }
903
+ else if (node.isBlock || node.isInline) {
904
+ const updatedChildren = loopContentAndMergeText(node.content, newTrackAttrs, schema);
905
+ return node.type.create({
907
906
  ...node.attrs,
908
- dataTracked: addTrackIdIfDoesntExist(deleteAttrs),
909
- };
910
- newTr.setNodeMarkup(pos, undefined, attrs, node.marks);
907
+ dataTracked: [addTrackIdIfDoesntExist(newTrackAttrs)],
908
+ }, prosemirrorModel.Fragment.fromArray(updatedChildren), node.marks);
909
+ }
910
+ else {
911
+ log.error(`unhandled node type: "${node.type.name}"`, node);
912
+ return node;
911
913
  }
912
914
  }
915
+ function setFragmentAsInserted(inserted, insertAttrs, schema) {
916
+ // Recurse the content in the inserted slice and either mark it tracked_insert or set node attrs
917
+ const updatedInserted = loopContentAndMergeText(inserted, insertAttrs, schema);
918
+ return updatedInserted.length === 0 ? prosemirrorModel.Fragment.empty : prosemirrorModel.Fragment.fromArray(updatedInserted);
919
+ }
920
+
921
+ /*!
922
+ * © 2021 Atypon Systems LLC
923
+ *
924
+ * Licensed under the Apache License, Version 2.0 (the "License");
925
+ * you may not use this file except in compliance with the License.
926
+ * You may obtain a copy of the License at
927
+ *
928
+ * http://www.apache.org/licenses/LICENSE-2.0
929
+ *
930
+ * Unless required by applicable law or agreed to in writing, software
931
+ * distributed under the License is distributed on an "AS IS" BASIS,
932
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
933
+ * See the License for the specific language governing permissions and
934
+ * limitations under the License.
935
+ */
936
+ function createNewInsertAttrs(attrs) {
937
+ return {
938
+ ...attrs,
939
+ operation: exports.CHANGE_OPERATION.insert,
940
+ };
941
+ }
942
+ function createNewDeleteAttrs(attrs) {
943
+ return {
944
+ ...attrs,
945
+ operation: exports.CHANGE_OPERATION.delete,
946
+ };
947
+ }
948
+ function createNewUpdateAttrs(attrs, oldAttrs) {
949
+ // Omit dataTracked
950
+ const { dataTracked, ...restAttrs } = oldAttrs;
951
+ return {
952
+ ...attrs,
953
+ operation: exports.CHANGE_OPERATION.set_node_attributes,
954
+ oldAttrs: restAttrs,
955
+ };
956
+ }
957
+
958
+ /*!
959
+ * © 2021 Atypon Systems LLC
960
+ *
961
+ * Licensed under the Apache License, Version 2.0 (the "License");
962
+ * you may not use this file except in compliance with the License.
963
+ * You may obtain a copy of the License at
964
+ *
965
+ * http://www.apache.org/licenses/LICENSE-2.0
966
+ *
967
+ * Unless required by applicable law or agreed to in writing, software
968
+ * distributed under the License is distributed on an "AS IS" BASIS,
969
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
970
+ * See the License for the specific language governing permissions and
971
+ * limitations under the License.
972
+ */
913
973
  /**
914
974
  * Applies deletion to the doc without actually deleting nodes that have not been inserted
915
975
  *
@@ -936,25 +996,20 @@ function deleteOrSetNodeDeleted(node, pos, newTr, deleteAttrs) {
936
996
  * @returns mapping adjusted by the applied operations & modified insert slice
937
997
  */
938
998
  function deleteAndMergeSplitNodes(from, to, gap, startDoc, newTr, schema, trackAttrs, insertSlice) {
939
- const deleteMap = new prosemirrorTransform.Mapping();
940
- const mergedInsertPos = undefined;
999
+ const steps = [];
941
1000
  // No deletion applied, return default values
942
1001
  if (from === to) {
943
1002
  return {
944
- deleteMap,
945
- mergedInsertPos,
946
1003
  newSliceContent: insertSlice.content,
1004
+ sliceWasSplit: false,
1005
+ steps,
947
1006
  };
948
1007
  }
949
1008
  const { openStart, openEnd } = insertSlice;
950
1009
  const { updatedSliceNodes, firstMergedNode, lastMergedNode } = splitSliceIntoMergedParts(insertSlice, gap !== undefined);
951
- const deleteAttrs = createNewDeleteAttrs(trackAttrs);
952
1010
  let mergingStartSide = true;
953
1011
  startDoc.nodesBetween(from, to, (node, pos) => {
954
- const { pos: offsetPos, deleted: nodeWasDeleted } = deleteMap.mapResult(pos, 1);
955
- const offsetFrom = deleteMap.map(from, -1);
956
- const offsetTo = deleteMap.map(to, 1);
957
- const nodeEnd = offsetPos + node.nodeSize;
1012
+ const nodeEnd = pos + node.nodeSize;
958
1013
  // So this insane boolean checks for ReplaceAroundStep gaps and whether the node should be skipped
959
1014
  // since the content inside gap should stay unchanged.
960
1015
  // All other nodes except text nodes consist of one start and end token (or just a single token for atoms).
@@ -965,23 +1020,13 @@ function deleteAndMergeSplitNodes(from, to, gap, startDoc, newTr, schema, trackA
965
1020
  // are altered and should not be skipped.
966
1021
  // @TODO ATM 20.7.2022 there doesn't seem to be tests that capture this.
967
1022
  const wasWithinGap = gap &&
968
- ((!node.isText && offsetPos >= deleteMap.map(gap.start, -1)) ||
969
- (node.isText &&
970
- offsetPos <= deleteMap.map(gap.start, -1) &&
971
- nodeEnd >= deleteMap.map(gap.end, -1)));
972
- let step = newTr.steps[newTr.steps.length - 1];
1023
+ ((!node.isText && pos >= gap.start) ||
1024
+ (node.isText && pos <= gap.start && nodeEnd >= gap.start));
973
1025
  // nodeEnd > offsetFrom -> delete touches this node
974
1026
  // eg (del 6 10) <p 5>|<t 6>cdf</t 9></p 10>| -> <p> nodeEnd 10 > from 6
975
- //
976
- // !nodeWasDeleted -> Check node wasn't already deleted by a previous deleteNode
977
- // This is quite tricky to wrap your head around and I've forgotten the nitty-gritty details already.
978
- // But from what I remember what it safeguards against is, when you've already deleted a node
979
- // say an inserted blockquote that had all its children deleted, nodesBetween still iterates over those
980
- // nodes and therefore we have to make this check to ensure they still exist in the doc.
981
- //
982
- if (nodeEnd > offsetFrom && !nodeWasDeleted && !wasWithinGap) {
1027
+ if (nodeEnd > from && !wasWithinGap) {
983
1028
  // |<p>asdf</p>| -> node deleted completely
984
- const nodeCompletelyDeleted = offsetPos >= offsetFrom && nodeEnd <= offsetTo;
1029
+ const nodeCompletelyDeleted = pos >= from && nodeEnd <= to;
985
1030
  // The end token deleted eg:
986
1031
  // <p 1>asdf|</p 7><p 7>bye</p 12>| + [<p>]hello</p> -> <p>asdfhello</p>
987
1032
  // (del 6 12) + (ins [<p>]hello</p> openStart 1 openEnd 0)
@@ -992,14 +1037,14 @@ function deleteAndMergeSplitNodes(from, to, gap, startDoc, newTr, schema, trackA
992
1037
  //
993
1038
  // What about:
994
1039
  // <p 1>asdf|</p 7><p 7 op="inserted">|bye</p 12> + empty -> <p>asdfbye</p>
995
- const endTokenDeleted = nodeEnd <= offsetTo;
1040
+ const endTokenDeleted = nodeEnd <= to;
996
1041
  // The start token deleted eg:
997
1042
  // |<p1 0>hey</p 6><p2 6>|asdf</p 12> + <p3>hello [</p>] -> <p3>hello asdf</p2>
998
1043
  // (del 0 7) + (ins <p>hello [</p>] openStart 0 openEnd 1)
999
1044
  // (<p1> pos 0) >= (from 0) && (nodeEnd 6) - 1 > (to 7) == false???
1000
1045
  // (<p2> pos 6) >= (from 0) && (nodeEnd 12) - 1 > (to 7) == true
1001
1046
  //
1002
- const startTokenDeleted = offsetPos >= offsetFrom; // && nodeEnd - 1 > offsetTo
1047
+ const startTokenDeleted = pos >= from; // && nodeEnd - 1 > offsetTo
1003
1048
  if (node.isText ||
1004
1049
  (!endTokenDeleted && startTokenDeleted) ||
1005
1050
  (endTokenDeleted && !startTokenDeleted)) {
@@ -1011,7 +1056,7 @@ function deleteAndMergeSplitNodes(from, to, gap, startDoc, newTr, schema, trackA
1011
1056
  }
1012
1057
  // Depth is often 1 when merging paragraphs or 2 for fully open blockquotes.
1013
1058
  // Incase of merging text within a ReplaceAroundStep the depth might be 1
1014
- const depth = newTr.doc.resolve(offsetPos).depth;
1059
+ const depth = newTr.doc.resolve(pos).depth;
1015
1060
  const mergeContent = mergingStartSide
1016
1061
  ? firstMergedNode === null || firstMergedNode === void 0 ? void 0 : firstMergedNode.mergedNodeContent
1017
1062
  : lastMergedNode === null || lastMergedNode === void 0 ? void 0 : lastMergedNode.mergedNodeContent;
@@ -1024,24 +1069,18 @@ function deleteAndMergeSplitNodes(from, to, gap, startDoc, newTr, schema, trackA
1024
1069
  // ProseMirror node semantics as start tokens are considered to contain the actual node itself.
1025
1070
  const mergeEndNode = startTokenDeleted && openEnd > 0 && depth === openEnd && mergeContent !== undefined;
1026
1071
  if (mergeStartNode || mergeEndNode) {
1027
- // The default insert position for block nodes is either the start of the merged content or the end.
1028
- // Incase text was merged, this must be updated as the start or end of the node doesn't map to the
1029
- // actual position of the merge. Currently the inserted content is inserted at the start or end
1030
- // of the merged content, TODO reverse the start/end when end/start token?
1031
- let insertPos = mergeStartNode ? nodeEnd - openStart : offsetPos + openEnd;
1032
- if (node.isText) {
1033
- // When merging text we must delete text in the same go as well, as the from/to boundary goes through
1034
- // the text node.
1035
- insertPos = deleteTextIfInserted(node, offsetPos, newTr, schema, deleteAttrs, offsetFrom, offsetTo);
1036
- deleteMap.appendMap(newTr.steps[newTr.steps.length - 1].getMap());
1037
- step = newTr.steps[newTr.steps.length - 1];
1038
- }
1039
1072
  // Just as a fun fact that I found out while debugging this. Inserting text at paragraph position wraps
1040
1073
  // it into a new paragraph(!). So that's why you always offset your positions to insert it _inside_
1041
1074
  // the paragraph.
1042
- if (mergeContent.size !== 0) {
1043
- newTr.insert(insertPos, setFragmentAsInserted(mergeContent, createNewInsertAttrs(trackAttrs), schema));
1044
- }
1075
+ steps.push({
1076
+ type: 'merge-fragment',
1077
+ pos,
1078
+ mergePos: mergeStartNode ? nodeEnd - openStart : pos + openEnd,
1079
+ from,
1080
+ to,
1081
+ node,
1082
+ fragment: setFragmentAsInserted(mergeContent, createNewInsertAttrs(trackAttrs), schema),
1083
+ });
1045
1084
  // Okay this is a bit ridiculous but it's used to adjust the insert pos when track changes prevents deletions
1046
1085
  // of merged nodes & content, as just using mapped toA in that case isn't the same.
1047
1086
  // The calculation is a bit mysterious, I admit.
@@ -1054,25 +1093,32 @@ function deleteAndMergeSplitNodes(from, to, gap, startDoc, newTr, schema, trackA
1054
1093
  else if (node.isText) {
1055
1094
  // Text deletion is handled even when the deletion doesn't completely wrap the text node
1056
1095
  // (which is basically the case most of the time)
1057
- deleteTextIfInserted(node, offsetPos, newTr, schema, deleteAttrs, offsetFrom, offsetTo);
1096
+ steps.push({
1097
+ type: 'delete-text',
1098
+ pos,
1099
+ from: Math.max(pos, from),
1100
+ to: Math.min(nodeEnd, to),
1101
+ node,
1102
+ });
1058
1103
  }
1059
1104
  else ;
1060
1105
  }
1061
1106
  else if (nodeCompletelyDeleted) {
1062
- deleteOrSetNodeDeleted(node, offsetPos, newTr, deleteAttrs);
1107
+ steps.push({
1108
+ type: 'delete-node',
1109
+ pos,
1110
+ nodeEnd,
1111
+ node,
1112
+ });
1063
1113
  }
1064
1114
  }
1065
- const newestStep = newTr.steps[newTr.steps.length - 1];
1066
- if (step !== newestStep) {
1067
- deleteMap.appendMap(newestStep.getMap());
1068
- }
1069
1115
  });
1070
1116
  return {
1071
- deleteMap,
1072
- mergedInsertPos,
1117
+ sliceWasSplit: !!(firstMergedNode || lastMergedNode),
1073
1118
  newSliceContent: updatedSliceNodes
1074
1119
  ? prosemirrorModel.Fragment.fromArray(updatedSliceNodes)
1075
1120
  : insertSlice.content,
1121
+ steps,
1076
1122
  };
1077
1123
  }
1078
1124
 
@@ -1100,8 +1146,7 @@ function mergeTrackedMarks(pos, doc, newTr, schema) {
1100
1146
  if (!shouldMergeTrackedAttributes(leftDataTracked, rightDataTracked)) {
1101
1147
  return;
1102
1148
  }
1103
- const isLeftOlder = (leftDataTracked.createdAt || Number.MAX_VALUE) <
1104
- (rightDataTracked.createdAt || Number.MAX_VALUE);
1149
+ const isLeftOlder = (leftDataTracked.createdAt || 0) < (rightDataTracked.createdAt || 0);
1105
1150
  const ancestorAttrs = isLeftOlder ? leftDataTracked : rightDataTracked;
1106
1151
  const dataTracked = {
1107
1152
  ...ancestorAttrs,
@@ -1157,14 +1202,16 @@ function trackReplaceAroundStep(step, oldState, newTr, attrs) {
1157
1202
  const stepResult = newTr.maybeStep(newStep);
1158
1203
  if (stepResult.failed) {
1159
1204
  log.error(`inverting ReplaceAroundStep failed: "${stepResult.failed}"`, newStep);
1160
- return;
1205
+ return [];
1161
1206
  }
1162
1207
  const gap = oldState.doc.slice(gapFrom, gapTo);
1163
1208
  log.info('RETAINED GAP CONTENT', gap);
1164
1209
  // First apply the deleted range and update the insert slice to not include content that was deleted,
1165
1210
  // eg partial nodes in an open-ended slice
1166
- const { deleteMap, newSliceContent } = deleteAndMergeSplitNodes(from, to, { start: gapFrom, end: gapTo }, newTr.doc, newTr, oldState.schema, attrs, slice);
1211
+ const { sliceWasSplit, newSliceContent, steps: deleteSteps, } = deleteAndMergeSplitNodes(from, to, { start: gapFrom, end: gapTo }, newTr.doc, newTr, oldState.schema, attrs, slice);
1212
+ let steps = deleteSteps;
1167
1213
  log.info('TR: new steps after applying delete', [...newTr.steps]);
1214
+ log.info('DELETE STEPS: ', deleteSteps);
1168
1215
  // We only want to insert when there something inside the gap (actually would this be always true?)
1169
1216
  // or insert slice wasn't just start/end tokens (which we already merged inside deleteAndMergeSplitBlockNodes)
1170
1217
  if (gap.size > 0 || (!structure && newSliceContent.size > 0)) {
@@ -1179,21 +1226,20 @@ function trackReplaceAroundStep(step, oldState, newTr, attrs) {
1179
1226
  insertedSlice = insertedSlice.insertAt(insertedSlice.size === 0 ? 0 : insert, gap.content);
1180
1227
  log.info('insertedSlice after inserted gap', insertedSlice);
1181
1228
  }
1182
- const newStep = new prosemirrorTransform.ReplaceStep(deleteMap.map(gapFrom), deleteMap.map(gapTo), insertedSlice, false);
1183
- const stepResult = newTr.maybeStep(newStep);
1184
- if (stepResult.failed) {
1185
- log.error(`insert ReplaceStep failed: "${stepResult.failed}"`, newStep);
1186
- return;
1187
- }
1188
- log.info('new steps after applying insert', [...newTr.steps]);
1189
- mergeTrackedMarks(deleteMap.map(gapFrom), newTr.doc, newTr, oldState.schema);
1190
- mergeTrackedMarks(deleteMap.map(gapTo), newTr.doc, newTr, oldState.schema);
1229
+ deleteSteps.push({
1230
+ type: 'insert-slice',
1231
+ from: gapFrom,
1232
+ to: gapTo,
1233
+ slice: insertedSlice,
1234
+ sliceWasSplit,
1235
+ });
1191
1236
  }
1192
1237
  else {
1193
1238
  // Incase only deletion was applied, check whether tracked marks around deleted content can be merged
1194
- mergeTrackedMarks(deleteMap.map(gapFrom), newTr.doc, newTr, oldState.schema);
1195
- mergeTrackedMarks(deleteMap.map(gapTo), newTr.doc, newTr, oldState.schema);
1239
+ mergeTrackedMarks(gapFrom, newTr.doc, newTr, oldState.schema);
1240
+ mergeTrackedMarks(gapTo, newTr.doc, newTr, oldState.schema);
1196
1241
  }
1242
+ return steps;
1197
1243
  }
1198
1244
 
1199
1245
  /*!
@@ -1213,7 +1259,7 @@ function trackReplaceAroundStep(step, oldState, newTr, attrs) {
1213
1259
  */
1214
1260
  function trackReplaceStep(step, oldState, newTr, attrs) {
1215
1261
  log.info('###### ReplaceStep ######');
1216
- let selectionPos = 0;
1262
+ let selectionPos = 0, changeSteps = [];
1217
1263
  step.getMap().forEach((fromA, toA, fromB, toB) => {
1218
1264
  log.info(`changed ranges: ${fromA} ${toA} ${fromB} ${toB}`);
1219
1265
  const { slice } = step;
@@ -1227,34 +1273,345 @@ function trackReplaceStep(step, oldState, newTr, attrs) {
1227
1273
  log.info('TR: steps before applying delete', [...newTr.steps]);
1228
1274
  // First apply the deleted range and update the insert slice to not include content that was deleted,
1229
1275
  // eg partial nodes in an open-ended slice
1230
- const { deleteMap, mergedInsertPos, newSliceContent } = deleteAndMergeSplitNodes(fromA, toA, undefined, oldState.doc, newTr, oldState.schema, attrs, slice);
1276
+ const { sliceWasSplit, newSliceContent, steps: deleteSteps, } = deleteAndMergeSplitNodes(fromA, toA, undefined, oldState.doc, newTr, oldState.schema, attrs, slice);
1277
+ changeSteps.push(...deleteSteps);
1231
1278
  log.info('TR: steps after applying delete', [...newTr.steps]);
1232
- const adjustedInsertPos = mergedInsertPos !== null && mergedInsertPos !== void 0 ? mergedInsertPos : deleteMap.map(toA);
1279
+ log.info('DELETE STEPS: ', changeSteps);
1280
+ const adjustedInsertPos = toA; // deleteMap.map(toA)
1233
1281
  if (newSliceContent.size > 0) {
1234
1282
  log.info('newSliceContent', newSliceContent);
1235
1283
  // Since deleteAndMergeSplitBlockNodes modified the slice to not to contain any merged nodes,
1236
1284
  // the sides should be equal. TODO can they be other than 0?
1237
1285
  const openStart = slice.openStart !== slice.openEnd ? 0 : slice.openStart;
1238
1286
  const openEnd = slice.openStart !== slice.openEnd ? 0 : slice.openEnd;
1239
- const insertedSlice = new prosemirrorModel.Slice(setFragmentAsInserted(newSliceContent, createNewInsertAttrs(attrs), oldState.schema), openStart, openEnd);
1240
- const newStep = new prosemirrorTransform.ReplaceStep(adjustedInsertPos, adjustedInsertPos, insertedSlice);
1287
+ changeSteps.push({
1288
+ type: 'insert-slice',
1289
+ from: adjustedInsertPos,
1290
+ to: adjustedInsertPos,
1291
+ sliceWasSplit,
1292
+ slice: new prosemirrorModel.Slice(setFragmentAsInserted(newSliceContent, createNewInsertAttrs(attrs), oldState.schema), openStart, openEnd),
1293
+ });
1294
+ }
1295
+ else {
1296
+ // Incase only deletion was applied, check whether tracked marks around deleted content can be merged
1297
+ mergeTrackedMarks(adjustedInsertPos, newTr.doc, newTr, oldState.schema);
1298
+ selectionPos = fromA;
1299
+ }
1300
+ });
1301
+ return [changeSteps, selectionPos];
1302
+ }
1303
+
1304
+ /*!
1305
+ * © 2021 Atypon Systems LLC
1306
+ *
1307
+ * Licensed under the Apache License, Version 2.0 (the "License");
1308
+ * you may not use this file except in compliance with the License.
1309
+ * You may obtain a copy of the License at
1310
+ *
1311
+ * http://www.apache.org/licenses/LICENSE-2.0
1312
+ *
1313
+ * Unless required by applicable law or agreed to in writing, software
1314
+ * distributed under the License is distributed on an "AS IS" BASIS,
1315
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1316
+ * See the License for the specific language governing permissions and
1317
+ * limitations under the License.
1318
+ */
1319
+ /**
1320
+ * Deletes inserted text directly, otherwise wraps it with tracked_delete mark
1321
+ *
1322
+ * This would work for general inline nodes too, but since node marks don't work properly
1323
+ * with Yjs, attributes are used instead.
1324
+ * @param node
1325
+ * @param pos
1326
+ * @param newTr
1327
+ * @param schema
1328
+ * @param deleteAttrs
1329
+ * @param from
1330
+ * @param to
1331
+ */
1332
+ function deleteTextIfInserted(node, pos, newTr, schema, deleteAttrs, from, to) {
1333
+ const start = from ? Math.max(pos, from) : pos;
1334
+ const nodeEnd = pos + node.nodeSize;
1335
+ const end = to ? Math.min(nodeEnd, to) : nodeEnd;
1336
+ if (node.marks.find((m) => m.type === schema.marks.tracked_insert)) {
1337
+ // Math.max(pos, from) is for picking always the start of the node,
1338
+ // not the start of the change (which might span multiple nodes).
1339
+ // Pos can be less than from as nodesBetween iterates through all nodes starting from the top block node
1340
+ newTr.replaceWith(start, end, prosemirrorModel.Fragment.empty);
1341
+ return start;
1342
+ }
1343
+ else {
1344
+ const leftNode = newTr.doc.resolve(start).nodeBefore;
1345
+ const leftMarks = getMergeableMarkTrackedAttrs(leftNode, deleteAttrs, schema);
1346
+ const rightNode = newTr.doc.resolve(end).nodeAfter;
1347
+ const rightMarks = getMergeableMarkTrackedAttrs(rightNode, deleteAttrs, schema);
1348
+ const fromStartOfMark = start - (leftNode && leftMarks ? leftNode.nodeSize : 0);
1349
+ const toEndOfMark = end + (rightNode && rightMarks ? rightNode.nodeSize : 0);
1350
+ const createdAt = Math.min((leftMarks === null || leftMarks === void 0 ? void 0 : leftMarks.createdAt) || Number.MAX_VALUE, (rightMarks === null || rightMarks === void 0 ? void 0 : rightMarks.createdAt) || Number.MAX_VALUE, deleteAttrs.createdAt);
1351
+ const dataTracked = addTrackIdIfDoesntExist({
1352
+ ...leftMarks,
1353
+ ...rightMarks,
1354
+ ...deleteAttrs,
1355
+ createdAt,
1356
+ });
1357
+ newTr.addMark(fromStartOfMark, toEndOfMark, schema.marks.tracked_delete.create({
1358
+ dataTracked,
1359
+ }));
1360
+ return toEndOfMark;
1361
+ }
1362
+ }
1363
+
1364
+ function processChangeSteps(changes, startPos, newTr, emptyAttrs, schema) {
1365
+ const mapping = new prosemirrorTransform.Mapping();
1366
+ const deleteAttrs = createNewDeleteAttrs(emptyAttrs);
1367
+ let selectionPos = startPos;
1368
+ // @TODO add custom handler / condition?
1369
+ changes.forEach((c) => {
1370
+ let step = newTr.steps[newTr.steps.length - 1];
1371
+ log.info('process change: ', c);
1372
+ // const handled = customStepHandler(changes, newTr, emptyAttrs) // ChangeStep[] | undefined
1373
+ if (c.type === 'delete-node') {
1374
+ deleteOrSetNodeDeleted(c.node, mapping.map(c.pos), newTr, deleteAttrs);
1375
+ const newestStep = newTr.steps[newTr.steps.length - 1];
1376
+ if (step !== newestStep) {
1377
+ mapping.appendMap(newestStep.getMap());
1378
+ step = newestStep;
1379
+ }
1380
+ mergeTrackedMarks(mapping.map(c.pos), newTr.doc, newTr, schema);
1381
+ }
1382
+ else if (c.type === 'delete-text') {
1383
+ const node = newTr.doc.nodeAt(mapping.map(c.pos));
1384
+ if (!node) {
1385
+ log.error(`processChangeSteps: no text node found for text-change`, c);
1386
+ return;
1387
+ }
1388
+ const where = deleteTextIfInserted(node, mapping.map(c.pos), newTr, schema, deleteAttrs, mapping.map(c.from), mapping.map(c.to));
1389
+ mergeTrackedMarks(where, newTr.doc, newTr, schema);
1390
+ }
1391
+ else if (c.type === 'merge-fragment') {
1392
+ let insertPos = mapping.map(c.mergePos);
1393
+ // The default insert position for block nodes is either the start of the merged content or the end.
1394
+ // Incase text was merged, this must be updated as the start or end of the node doesn't map to the
1395
+ // actual position of the merge. Currently the inserted content is inserted at the start or end
1396
+ // of the merged content, TODO reverse the start/end when end/start token?
1397
+ if (c.node.isText) {
1398
+ // When merging text we must delete text in the same go as well, as the from/to boundary goes through
1399
+ // the text node.
1400
+ insertPos = deleteTextIfInserted(c.node, mapping.map(c.pos), newTr, schema, deleteAttrs, mapping.map(c.from), mapping.map(c.to));
1401
+ const newestStep = newTr.steps[newTr.steps.length - 1];
1402
+ if (step !== newestStep) {
1403
+ mapping.appendMap(newestStep.getMap());
1404
+ step = newestStep;
1405
+ }
1406
+ }
1407
+ if (c.fragment.size > 0) {
1408
+ newTr.insert(insertPos, c.fragment);
1409
+ }
1410
+ }
1411
+ else if (c.type === 'insert-slice') {
1412
+ const newStep = new prosemirrorTransform.ReplaceStep(mapping.map(c.from), mapping.map(c.to), c.slice, false);
1241
1413
  const stepResult = newTr.maybeStep(newStep);
1242
1414
  if (stepResult.failed) {
1243
- log.error(`insert ReplaceStep failed: "${stepResult.failed}"`, newStep);
1415
+ log.error(`processChangeSteps: insert-slice ReplaceStep failed "${stepResult.failed}"`, newStep);
1244
1416
  return;
1245
1417
  }
1246
- log.info('new steps after applying insert', [...newTr.steps]);
1247
- mergeTrackedMarks(adjustedInsertPos, newTr.doc, newTr, oldState.schema);
1248
- mergeTrackedMarks(adjustedInsertPos + insertedSlice.size, newTr.doc, newTr, oldState.schema);
1249
- selectionPos = adjustedInsertPos + insertedSlice.size;
1418
+ mergeTrackedMarks(mapping.map(c.from), newTr.doc, newTr, schema);
1419
+ mergeTrackedMarks(mapping.map(c.to), newTr.doc, newTr, schema);
1420
+ selectionPos = mapping.map(c.to) + c.slice.size;
1421
+ }
1422
+ else if (c.type === 'update-node-attrs') {
1423
+ const oldDataTracked = getBlockInlineTrackedData(c.node) || [];
1424
+ const oldUpdate = oldDataTracked.find((d) => d.operation === exports.CHANGE_OPERATION.set_node_attributes);
1425
+ const { dataTracked, ...oldAttrs } = (oldUpdate === null || oldUpdate === void 0 ? void 0 : oldUpdate.oldAttrs) || c.node.attrs;
1426
+ const newDataTracked = [...oldDataTracked.filter((d) => !oldUpdate || d.id !== oldUpdate.id)];
1427
+ const newUpdate = oldUpdate
1428
+ ? {
1429
+ ...oldUpdate,
1430
+ updatedAt: emptyAttrs.updatedAt,
1431
+ }
1432
+ : addTrackIdIfDoesntExist(createNewUpdateAttrs(emptyAttrs, c.node.attrs));
1433
+ // Dont add update changes if there exists already an insert change for this node
1434
+ if (JSON.stringify(oldAttrs) !== JSON.stringify(c.newAttrs) &&
1435
+ !oldDataTracked.find((d) => d.operation === exports.CHANGE_OPERATION.insert)) {
1436
+ newDataTracked.push(newUpdate);
1437
+ }
1438
+ newTr.setNodeMarkup(mapping.map(c.pos), undefined, {
1439
+ ...c.newAttrs,
1440
+ dataTracked: newDataTracked.length > 0 ? newDataTracked : null,
1441
+ }, c.node.marks);
1442
+ }
1443
+ const newestStep = newTr.steps[newTr.steps.length - 1];
1444
+ if (step !== newestStep) {
1445
+ mapping.appendMap(newestStep.getMap());
1446
+ }
1447
+ });
1448
+ return [mapping, selectionPos];
1449
+ }
1450
+
1451
+ function matchText(adjDeleted, insNode, offset, matchedDeleted, deleted) {
1452
+ const { pos, from, to, node: delNode } = adjDeleted;
1453
+ let j = offset, d = from - pos, maxSteps = to - Math.max(pos, from);
1454
+ // Match text inside the inserted text node to the deleted text node
1455
+ for (; maxSteps !== j && insNode.text[j] !== undefined && insNode.text[j] === delNode.text[d]; j += 1, d += 1) {
1456
+ matchedDeleted += 1;
1457
+ }
1458
+ // this is needed incase diffing tr.doc
1459
+ // deleted.push({
1460
+ // pos: pos,
1461
+ // type: 'update-node-attrs',
1462
+ // // Should check the attrs for equality in fixInconsistentChanges? to remove dataTracked completely
1463
+ // oldAttrs: adjDeleted.node.attrs || {},
1464
+ // newAttrs: child.attrs || {},
1465
+ // })
1466
+ deleted = deleted.filter((d) => d !== adjDeleted);
1467
+ if (maxSteps !== j) {
1468
+ deleted.push({
1469
+ pos,
1470
+ from: from + j - offset,
1471
+ to,
1472
+ type: 'delete-text',
1473
+ node: delNode,
1474
+ });
1475
+ return [matchedDeleted, deleted];
1476
+ }
1477
+ const nextTextDelete = deleted.find((d) => d.type === 'delete-text' && d.pos === to);
1478
+ if (nextTextDelete) {
1479
+ return matchText(nextTextDelete, insNode, j, matchedDeleted, deleted);
1480
+ }
1481
+ return [matchedDeleted, deleted];
1482
+ }
1483
+ function matchInserted(matchedDeleted, deleted, inserted) {
1484
+ var _a;
1485
+ let matched = [matchedDeleted, deleted];
1486
+ for (let i = 0;; i += 1) {
1487
+ if (inserted.childCount === i)
1488
+ return matched;
1489
+ const insNode = inserted.child(i);
1490
+ // @ts-ignore
1491
+ let adjDeleted = matched[1].find((d) => (d.type === 'delete-text' && Math.max(d.pos, d.from) === matched[0]) ||
1492
+ (d.type === 'delete-node' && d.pos === matched[0]));
1493
+ if (insNode.type !== ((_a = adjDeleted === null || adjDeleted === void 0 ? void 0 : adjDeleted.node) === null || _a === void 0 ? void 0 : _a.type)) {
1494
+ return matched;
1495
+ }
1496
+ else if (insNode.isText && (adjDeleted === null || adjDeleted === void 0 ? void 0 : adjDeleted.node)) {
1497
+ matched = matchText(adjDeleted, insNode, 0, matched[0], matched[1]);
1498
+ continue;
1499
+ }
1500
+ else if (insNode.content.size > 0 || (adjDeleted === null || adjDeleted === void 0 ? void 0 : adjDeleted.node.content.size) > 0) {
1501
+ // Move the inDeleted inside the block/inline node's boundary
1502
+ matched = matchInserted(matched[0] + 1, matched[1].filter((d) => d !== adjDeleted), insNode.content);
1250
1503
  }
1251
1504
  else {
1252
- // Incase only deletion was applied, check whether tracked marks around deleted content can be merged
1253
- mergeTrackedMarks(adjustedInsertPos, newTr.doc, newTr, oldState.schema);
1254
- selectionPos = fromA;
1505
+ matched = [matched[0] + insNode.nodeSize, matched[1].filter((d) => d !== adjDeleted)];
1506
+ }
1507
+ // Omit dataTracked
1508
+ const { dataTracked, ...newAttrs } = insNode.attrs || {};
1509
+ matched[1].push({
1510
+ pos: adjDeleted.pos,
1511
+ type: 'update-node-attrs',
1512
+ node: adjDeleted.node,
1513
+ newAttrs,
1514
+ });
1515
+ }
1516
+ }
1517
+
1518
+ /*!
1519
+ * © 2021 Atypon Systems LLC
1520
+ *
1521
+ * Licensed under the Apache License, Version 2.0 (the "License");
1522
+ * you may not use this file except in compliance with the License.
1523
+ * You may obtain a copy of the License at
1524
+ *
1525
+ * http://www.apache.org/licenses/LICENSE-2.0
1526
+ *
1527
+ * Unless required by applicable law or agreed to in writing, software
1528
+ * distributed under the License is distributed on an "AS IS" BASIS,
1529
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1530
+ * See the License for the specific language governing permissions and
1531
+ * limitations under the License.
1532
+ */
1533
+ /**
1534
+ * Cuts a fragment similar to Fragment.cut but also removes the parent node.
1535
+ *
1536
+ * @TODO there is however, some silly calculation mistake so that I need to use matched - deleted + 1 > 0
1537
+ * inside it to check whether to actually cut a text node. The offset might be cascading, therefore it should
1538
+ * be fixed at some point.
1539
+ * @param matched
1540
+ * @param deleted
1541
+ * @param content
1542
+ * @returns
1543
+ */
1544
+ function cutFragment(matched, deleted, content) {
1545
+ let newContent = [];
1546
+ for (let i = 0; matched <= deleted && i < content.childCount; i += 1) {
1547
+ const child = content.child(i);
1548
+ if (!child.isText && child.content.size > 0) {
1549
+ const cut = cutFragment(matched + 1, deleted, child.content);
1550
+ matched = cut[0];
1551
+ newContent.push(...cut[1].content);
1552
+ }
1553
+ else if (child.isText && matched + child.nodeSize > deleted) {
1554
+ if (deleted - matched > 0) {
1555
+ newContent.push(child.cut(deleted - matched));
1556
+ }
1557
+ else {
1558
+ newContent.push(child);
1559
+ }
1560
+ matched = deleted + 1;
1561
+ }
1562
+ else {
1563
+ matched += child.nodeSize;
1564
+ }
1565
+ }
1566
+ return [matched, prosemirrorModel.Fragment.fromArray(newContent)];
1567
+ }
1568
+ function diffChangeSteps(deleted, inserted, newTr, schema) {
1569
+ const updated = [];
1570
+ let updatedDeleted = [...deleted];
1571
+ inserted.forEach((ins) => {
1572
+ log.info('DIFF ins ', ins);
1573
+ //
1574
+ // @TODO this is a temporary workaround to prevent duplicated diffing between splitSliceIntoMergedParts and
1575
+ // matchInserted.
1576
+ //
1577
+ // As originally authored splitSliceIntoMergedParts splits open slices into their merged parts
1578
+ // leaving out the need to insert the possibly deleted nodes into the doc. However, as matchInserted now
1579
+ // traverses the deleted range checking it against the inserted slice this behaves quite in a same way
1580
+ // where the opened block nodes are traversed but left unmodified. With an openStart > 0 though the
1581
+ // node-attr-updates would additionally have to be filtered out in the processChangeSteps.
1582
+ //
1583
+ // The old logic is still left as it's as refactoring is painful and would probably break something and just
1584
+ // in general, take a lot of time. Therefore, this sliceWasSplit boolean is used to just skip diffing.
1585
+ if (ins.sliceWasSplit) {
1586
+ updated.push(ins);
1587
+ return;
1588
+ }
1589
+ // Start diffing from the start of the deleted range
1590
+ const deleteStart = updatedDeleted.reduce((acc, cur) => {
1591
+ if (cur.type === 'delete-node') {
1592
+ return Math.min(acc, cur.pos);
1593
+ }
1594
+ else if (cur.type === 'delete-text') {
1595
+ return Math.min(acc, cur.from);
1596
+ }
1597
+ return acc;
1598
+ }, Number.MAX_SAFE_INTEGER);
1599
+ const [matchedDeleted, updatedDel] = matchInserted(deleteStart, updatedDeleted, ins.slice.content);
1600
+ if (matchedDeleted === deleteStart) {
1601
+ updated.push(ins);
1602
+ return;
1603
+ }
1604
+ updatedDeleted = updatedDel;
1605
+ const [_, newInserted] = cutFragment(0, matchedDeleted - deleteStart, ins.slice.content);
1606
+ if (newInserted.size > 0) {
1607
+ updated.push({
1608
+ ...ins,
1609
+ slice: new prosemirrorModel.Slice(newInserted, ins.slice.openStart, ins.slice.openEnd),
1610
+ });
1255
1611
  }
1256
1612
  });
1257
- return selectionPos;
1613
+ log.info('FINISH DIFF: ', [...updatedDeleted, ...updated]);
1614
+ return [...updatedDeleted, ...updated];
1258
1615
  }
1259
1616
 
1260
1617
  /**
@@ -1308,7 +1665,15 @@ function trackTransaction(tr, oldState, newTr, authorID) {
1308
1665
  return;
1309
1666
  }
1310
1667
  else if (step instanceof prosemirrorTransform.ReplaceStep) {
1311
- const selectionPos = trackReplaceStep(step, oldState, newTr, emptyAttrs);
1668
+ let [steps, startPos] = trackReplaceStep(step, oldState, newTr, emptyAttrs);
1669
+ log.info('CHANGES: ', steps);
1670
+ // deleted and merged really...
1671
+ const deleted = steps.filter((s) => s.type !== 'insert-slice');
1672
+ const inserted = steps.filter((s) => s.type === 'insert-slice');
1673
+ steps = diffChangeSteps(deleted, inserted, newTr, oldState.schema);
1674
+ log.info('DIFFED STEPS: ', steps);
1675
+ const [mapping, selectionPos] = processChangeSteps(steps, startPos || tr.selection.head, // Incase startPos is it's default value 0, use the old selection head
1676
+ newTr, emptyAttrs, oldState.schema);
1312
1677
  if (!wasNodeSelection) {
1313
1678
  const sel = getSelectionStaticConstructor(tr.selection);
1314
1679
  // Use Selection.near to fix selections that point to a block node instead of inline content
@@ -1319,10 +1684,16 @@ function trackTransaction(tr, oldState, newTr, authorID) {
1319
1684
  }
1320
1685
  }
1321
1686
  else if (step instanceof prosemirrorTransform.ReplaceAroundStep) {
1322
- trackReplaceAroundStep(step, oldState, newTr, emptyAttrs);
1323
- // } else if (step instanceof AddMarkStep) {
1324
- // } else if (step instanceof RemoveMarkStep) {
1687
+ let steps = trackReplaceAroundStep(step, oldState, newTr, emptyAttrs);
1688
+ const deleted = steps.filter((s) => s.type !== 'insert-slice');
1689
+ const inserted = steps.filter((s) => s.type === 'insert-slice');
1690
+ log.info('INSERT STEPS: ', inserted);
1691
+ steps = diffChangeSteps(deleted, inserted, newTr, oldState.schema);
1692
+ log.info('DIFFED STEPS: ', steps);
1693
+ processChangeSteps(steps, tr.selection.from, newTr, emptyAttrs, oldState.schema);
1325
1694
  }
1695
+ // } else if (step instanceof AddMarkStep) {
1696
+ // } else if (step instanceof RemoveMarkStep) {
1326
1697
  // TODO: here we could check whether adjacent inserts & deletes cancel each other out.
1327
1698
  // However, this should not be done by diffing and only matching node or char by char instead since
1328
1699
  // it's A easier and B more intuitive to user.
@@ -1456,13 +1827,13 @@ const trackChangesPlugin = (opts = { userID: 'anonymous:Anonymous' }) => {
1456
1827
  ids.forEach((changeId) => {
1457
1828
  const change = changeSet === null || changeSet === void 0 ? void 0 : changeSet.get(changeId);
1458
1829
  if (change) {
1459
- createdTr = updateChangeAttrs(createdTr, change, { status, reviewedByID: userID }, oldState.schema);
1460
- setAction(createdTr, TrackChangesAction.updateChanges, [change.id]);
1830
+ createdTr = updateChangeAttrs(createdTr, change, { ...change.dataTracked, status, reviewedByID: userID }, oldState.schema);
1461
1831
  }
1462
1832
  });
1833
+ setAction(createdTr, TrackChangesAction.updateChanges, ids);
1463
1834
  }
1464
1835
  else if (getAction(tr, TrackChangesAction.applyAndRemoveChanges)) {
1465
- const mapping = applyAcceptedRejectedChanges(createdTr, oldState.schema, changeSet.nodeChanges);
1836
+ const mapping = applyAcceptedRejectedChanges(createdTr, oldState.schema, changeSet.bothNodeChanges);
1466
1837
  applyAcceptedRejectedChanges(createdTr, oldState.schema, changeSet.textChanges, mapping);
1467
1838
  setAction(createdTr, TrackChangesAction.refreshChanges, true);
1468
1839
  }
@@ -1481,21 +1852,6 @@ const trackChangesPlugin = (opts = { userID: 'anonymous:Anonymous' }) => {
1481
1852
  });
1482
1853
  };
1483
1854
 
1484
- /*!
1485
- * © 2021 Atypon Systems LLC
1486
- *
1487
- * Licensed under the Apache License, Version 2.0 (the "License");
1488
- * you may not use this file except in compliance with the License.
1489
- * You may obtain a copy of the License at
1490
- *
1491
- * http://www.apache.org/licenses/LICENSE-2.0
1492
- *
1493
- * Unless required by applicable law or agreed to in writing, software
1494
- * distributed under the License is distributed on an "AS IS" BASIS,
1495
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1496
- * See the License for the specific language governing permissions and
1497
- * limitations under the License.
1498
- */
1499
1855
  /**
1500
1856
  * Sets track-changes plugin's status to any of: 'enabled' 'disabled' 'viewSnapshots'. Passing undefined will
1501
1857
  * set 'enabled' status to 'disabled' and 'disabled' | 'viewSnapshots' status to 'enabled'.