@manuscripts/track-changes-plugin 0.0.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,17 +1,9 @@
1
- 'use strict';
1
+ import { PluginKey, Plugin } from 'prosemirror-state';
2
+ import debug from 'debug';
3
+ import { liftTarget, canJoin, Mapping, ReplaceStep, ReplaceAroundStep } from 'prosemirror-transform';
4
+ import { Fragment, Slice } from 'prosemirror-model';
2
5
 
3
- Object.defineProperty(exports, '__esModule', { value: true });
4
-
5
- var prosemirrorState = require('prosemirror-state');
6
- var debug = require('debug');
7
- var prosemirrorTransform = require('prosemirror-transform');
8
- var prosemirrorModel = require('prosemirror-model');
9
-
10
- function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
11
-
12
- var debug__default = /*#__PURE__*/_interopDefaultLegacy(debug);
13
-
14
- exports.TrackChangesAction = void 0;
6
+ var TrackChangesAction;
15
7
  (function (TrackChangesAction) {
16
8
  TrackChangesAction["skipTrack"] = "track-changes-skip-tracking";
17
9
  TrackChangesAction["setUserID"] = "track-changes-set-user-id";
@@ -20,7 +12,7 @@ exports.TrackChangesAction = void 0;
20
12
  TrackChangesAction["updateChanges"] = "track-changes-update-changes";
21
13
  TrackChangesAction["refreshChanges"] = "track-changes-refresh-changes";
22
14
  TrackChangesAction["applyAndRemoveChanges"] = "track-changes-apply-remove-changes";
23
- })(exports.TrackChangesAction || (exports.TrackChangesAction = {}));
15
+ })(TrackChangesAction || (TrackChangesAction = {}));
24
16
  /**
25
17
  * Gets the value of a meta field, action payload, of a defined track-changes action.
26
18
  * @param tr
@@ -38,7 +30,14 @@ function getAction(tr, action) {
38
30
  */
39
31
  function setAction(tr, action, payload) {
40
32
  return tr.setMeta(action, payload);
41
- }
33
+ }
34
+ /**
35
+ * Skip tracking for a transaction, use this with caution to avoid race-conditions or just to otherwise
36
+ * omitting applying of track attributes or marks.
37
+ * @param tr
38
+ * @returns
39
+ */
40
+ const skipTracking = (tr) => setAction(tr, TrackChangesAction.skipTrack, true);
42
41
 
43
42
  /******************************************************************************
44
43
  Copyright (c) Microsoft Corporation.
@@ -83,7 +82,7 @@ function __classPrivateFieldSet(receiver, state, value, kind, f) {
83
82
  * See the License for the specific language governing permissions and
84
83
  * limitations under the License.
85
84
  */
86
- exports.CHANGE_OPERATION = void 0;
85
+ var CHANGE_OPERATION;
87
86
  (function (CHANGE_OPERATION) {
88
87
  CHANGE_OPERATION["insert"] = "insert";
89
88
  CHANGE_OPERATION["delete"] = "delete";
@@ -92,13 +91,13 @@ exports.CHANGE_OPERATION = void 0;
92
91
  CHANGE_OPERATION["unwrap_from_node"] = "unwrap_from_node";
93
92
  CHANGE_OPERATION["add_mark"] = "add_mark";
94
93
  CHANGE_OPERATION["remove_mark"] = "remove_mark";
95
- })(exports.CHANGE_OPERATION || (exports.CHANGE_OPERATION = {}));
96
- exports.CHANGE_STATUS = void 0;
94
+ })(CHANGE_OPERATION || (CHANGE_OPERATION = {}));
95
+ var CHANGE_STATUS;
97
96
  (function (CHANGE_STATUS) {
98
97
  CHANGE_STATUS["accepted"] = "accepted";
99
98
  CHANGE_STATUS["rejected"] = "rejected";
100
99
  CHANGE_STATUS["pending"] = "pending";
101
- })(exports.CHANGE_STATUS || (exports.CHANGE_STATUS = {}));
100
+ })(CHANGE_STATUS || (CHANGE_STATUS = {}));
102
101
 
103
102
  /*!
104
103
  * © 2021 Atypon Systems LLC
@@ -115,7 +114,7 @@ exports.CHANGE_STATUS = void 0;
115
114
  * See the License for the specific language governing permissions and
116
115
  * limitations under the License.
117
116
  */
118
- const logger = debug__default["default"]('track');
117
+ const logger = debug('track');
119
118
  const log = {
120
119
  info(str, obj) {
121
120
  if (obj) {
@@ -148,10 +147,10 @@ const log = {
148
147
  */
149
148
  const enableDebug = (enabled) => {
150
149
  if (enabled) {
151
- debug__default["default"].enable('track');
150
+ debug.enable('track');
152
151
  }
153
152
  else {
154
- debug__default["default"].disable();
153
+ debug.disable();
155
154
  }
156
155
  };
157
156
 
@@ -211,13 +210,13 @@ class ChangeSet {
211
210
  return rootNodes;
212
211
  }
213
212
  get pending() {
214
- return this.changeTree.filter((c) => c.attrs.status === exports.CHANGE_STATUS.pending);
213
+ return this.changeTree.filter((c) => c.attrs.status === CHANGE_STATUS.pending);
215
214
  }
216
215
  get accepted() {
217
- return this.changeTree.filter((c) => c.attrs.status === exports.CHANGE_STATUS.accepted);
216
+ return this.changeTree.filter((c) => c.attrs.status === CHANGE_STATUS.accepted);
218
217
  }
219
218
  get rejected() {
220
- return this.changeTree.filter((c) => c.attrs.status === exports.CHANGE_STATUS.rejected);
219
+ return this.changeTree.filter((c) => c.attrs.status === CHANGE_STATUS.rejected);
221
220
  }
222
221
  get textChanges() {
223
222
  return this.changes.filter((c) => c.type === 'text-change');
@@ -271,8 +270,8 @@ class ChangeSet {
271
270
  */
272
271
  static shouldNotDelete(change) {
273
272
  const { status, operation } = change.attrs;
274
- return ((operation === exports.CHANGE_OPERATION.insert && status === exports.CHANGE_STATUS.accepted) ||
275
- (operation === exports.CHANGE_OPERATION.delete && status === exports.CHANGE_STATUS.rejected));
273
+ return ((operation === CHANGE_OPERATION.insert && status === CHANGE_STATUS.accepted) ||
274
+ (operation === CHANGE_OPERATION.delete && status === CHANGE_STATUS.rejected));
276
275
  }
277
276
  /**
278
277
  * Determines whether a change should be deleted when applying it to the document.
@@ -280,8 +279,8 @@ class ChangeSet {
280
279
  */
281
280
  static shouldDeleteChange(change) {
282
281
  const { status, operation } = change.attrs;
283
- return ((operation === exports.CHANGE_OPERATION.insert && status === exports.CHANGE_STATUS.rejected) ||
284
- (operation === exports.CHANGE_OPERATION.delete && status === exports.CHANGE_STATUS.accepted));
282
+ return ((operation === CHANGE_OPERATION.insert && status === CHANGE_STATUS.rejected) ||
283
+ (operation === CHANGE_OPERATION.delete && status === CHANGE_STATUS.accepted));
285
284
  }
286
285
  /**
287
286
  * Checks whether change attributes contain all TrackedAttrs keys with non-undefined values
@@ -291,10 +290,22 @@ class ChangeSet {
291
290
  if ('attrs' in attrs) {
292
291
  log.warn('passed "attrs" as property to isValidTrackedAttrs(attrs)', attrs);
293
292
  }
294
- const trackedKeys = ['id', 'userID', 'operation', 'status', 'createdAt'];
295
- const entries = Object.entries(attrs);
293
+ const trackedKeys = [
294
+ 'id',
295
+ 'authorID',
296
+ 'operation',
297
+ 'status',
298
+ 'createdAt',
299
+ 'updatedAt',
300
+ ];
301
+ // reviewedByID is set optional since either ProseMirror or Yjs doesn't like persisting null values inside attributes objects
302
+ // So it can be either omitted completely or at least null or string
303
+ const optionalKeys = ['reviewedByID'];
304
+ const entries = Object.entries(attrs).filter(([key, val]) => trackedKeys.includes(key));
305
+ const optionalEntries = Object.entries(attrs).filter(([key, val]) => optionalKeys.includes(key));
296
306
  return (entries.length === trackedKeys.length &&
297
307
  entries.every(([key, val]) => trackedKeys.includes(key) && val !== undefined) &&
308
+ optionalEntries.every(([key, val]) => optionalKeys.includes(key) && val !== undefined) &&
298
309
  (attrs.id || '').length > 0 // Changes created with undefined id have '' as placeholder
299
310
  );
300
311
  }
@@ -335,14 +346,17 @@ function deleteNode(node, pos, tr) {
335
346
  var _a;
336
347
  const startPos = tr.doc.resolve(pos + 1);
337
348
  const range = startPos.blockRange(tr.doc.resolve(startPos.pos - 2 + node.nodeSize));
338
- const targetDepth = range ? Number(prosemirrorTransform.liftTarget(range)) : NaN;
339
- if (range && !Number.isNaN(targetDepth)) {
349
+ const targetDepth = range && liftTarget(range);
350
+ // Check with typeof since with prosemirror-transform pre 1.6.0 targetDepth is undefined
351
+ if (range && typeof targetDepth === 'number') {
340
352
  return tr.lift(range, targetDepth);
341
353
  }
342
354
  const resPos = tr.doc.resolve(pos);
343
- const canMergeToNodeAbove = (resPos.parent !== tr.doc || resPos.nodeBefore) && ((_a = node.firstChild) === null || _a === void 0 ? void 0 : _a.isText);
355
+ // Block nodes can be deleted by just removing their start token which should then merge the text
356
+ // content to above node's content (if there is one)
357
+ const canMergeToNodeAbove = (resPos.parent !== tr.doc || resPos.nodeBefore) && node.isBlock && ((_a = node.firstChild) === null || _a === void 0 ? void 0 : _a.isText);
344
358
  if (canMergeToNodeAbove) {
345
- return tr.replaceWith(pos - 1, pos + 1, prosemirrorModel.Fragment.empty);
359
+ return tr.replaceWith(pos - 1, pos + 1, Fragment.empty);
346
360
  }
347
361
  else {
348
362
  // NOTE: there's an edge case where moving content is not possible but because the immediate
@@ -375,17 +389,18 @@ function deleteNode(node, pos, tr) {
375
389
  */
376
390
  function mergeNode(node, pos, tr) {
377
391
  var _a;
378
- if (prosemirrorTransform.canJoin(tr.doc, pos)) {
392
+ if (canJoin(tr.doc, pos)) {
379
393
  return tr.join(pos);
380
394
  }
381
- else if (prosemirrorTransform.canJoin(tr.doc, pos + node.nodeSize)) {
395
+ else if (canJoin(tr.doc, pos + node.nodeSize)) {
396
+ // TODO should copy the attributes from the merged node below
382
397
  return tr.join(pos + node.nodeSize);
383
398
  }
384
- // TODO is this the same thing as join?
399
+ // TODO is this the same thing as join to above?
385
400
  const resPos = tr.doc.resolve(pos);
386
401
  const canMergeToNodeAbove = (resPos.parent !== tr.doc || resPos.nodeBefore) && ((_a = node.firstChild) === null || _a === void 0 ? void 0 : _a.isText);
387
402
  if (canMergeToNodeAbove) {
388
- return tr.replaceWith(pos - 1, pos + 1, prosemirrorModel.Fragment.empty);
403
+ return tr.replaceWith(pos - 1, pos + 1, Fragment.empty);
389
404
  }
390
405
  return undefined;
391
406
  }
@@ -429,8 +444,8 @@ function getInlineNodeTrackedMarkData(node, schema) {
429
444
  node.marks.forEach((mark) => {
430
445
  if (mark.type === schema.marks.tracked_insert || mark.type === schema.marks.tracked_delete) {
431
446
  const operation = mark.type === schema.marks.tracked_insert
432
- ? exports.CHANGE_OPERATION.insert
433
- : exports.CHANGE_OPERATION.delete;
447
+ ? CHANGE_OPERATION.insert
448
+ : CHANGE_OPERATION.delete;
434
449
  marksTrackedData.push({ ...mark.attrs.dataTracked, operation });
435
450
  }
436
451
  });
@@ -460,7 +475,7 @@ function shouldMergeTrackedAttributes(left, right) {
460
475
  }
461
476
  return (left.status === right.status &&
462
477
  left.operation === right.operation &&
463
- left.userID === right.userID);
478
+ left.authorID === right.authorID);
464
479
  }
465
480
  function getMergeableMarkTrackedAttrs(node, attrs, schema) {
466
481
  const nodeAttrs = getInlineNodeTrackedMarkData(node, schema);
@@ -504,16 +519,16 @@ function updateChangeChildrenAttributes(changes, tr, mapping) {
504
519
  * @param changes
505
520
  * @param deleteMap
506
521
  */
507
- function applyAcceptedRejectedChanges(tr, schema, changes, deleteMap = new prosemirrorTransform.Mapping()) {
522
+ function applyAcceptedRejectedChanges(tr, schema, changes, deleteMap = new Mapping()) {
508
523
  changes.forEach((change) => {
509
- if (change.attrs.status === exports.CHANGE_STATUS.pending) {
524
+ if (change.attrs.status === CHANGE_STATUS.pending) {
510
525
  return;
511
526
  }
512
527
  // Map change.from and skip those which dont need to be applied
513
528
  // or were already deleted by an applied block delete
514
529
  const { pos: from, deleted } = deleteMap.mapResult(change.from), node = tr.doc.nodeAt(from), noChangeNeeded = deleted || ChangeSet.shouldNotDelete(change);
515
530
  if (!node) {
516
- log.warn('no node found to update for change', change);
531
+ !deleted && log.warn('no node found to update for change', change);
517
532
  return;
518
533
  }
519
534
  if (ChangeSet.isTextChange(change) && noChangeNeeded) {
@@ -580,6 +595,7 @@ function findChanges(state) {
580
595
  type: 'text-change',
581
596
  from: pos,
582
597
  to: pos + node.nodeSize,
598
+ text: node.text,
583
599
  attrs,
584
600
  },
585
601
  node,
@@ -624,13 +640,15 @@ function fixInconsistentChanges(changeSet, trackUserID, newTr, schema) {
624
640
  const iteratedIds = new Set();
625
641
  let changed = false;
626
642
  changeSet.invalidChanges.forEach((c) => {
627
- const { id, userID, operation, status, createdAt } = c.attrs;
643
+ const { id, authorID, operation, reviewedByID, status, createdAt, updatedAt } = c.attrs;
628
644
  const newAttrs = {
629
645
  ...((!id || iteratedIds.has(id) || id.length === 0) && { id: uuidv4() }),
630
- ...(!userID && { userID: trackUserID }),
631
- ...(!operation && { operation: exports.CHANGE_OPERATION.insert }),
632
- ...(!status && { status: exports.CHANGE_STATUS.pending }),
646
+ ...(!authorID && { authorID: trackUserID }),
647
+ ...(!operation && { operation: CHANGE_OPERATION.insert }),
648
+ ...(!reviewedByID && { reviewedByID: null }),
649
+ ...(!status && { status: CHANGE_STATUS.pending }),
633
650
  ...(!createdAt && { createdAt: Date.now() }),
651
+ ...(!updatedAt && { updatedAt: Date.now() }),
634
652
  };
635
653
  if (Object.keys(newAttrs).length > 0) {
636
654
  updateChangeAttrs(newTr, c, { ...c.attrs, ...newAttrs }, schema);
@@ -658,7 +676,7 @@ function fixInconsistentChanges(changeSet, trackUserID, newTr, schema) {
658
676
  */
659
677
  function markInlineNodeChange(node, newTrackAttrs, schema) {
660
678
  const filtered = node.marks.filter((m) => m.type !== schema.marks.tracked_insert && m.type !== schema.marks.tracked_delete);
661
- const mark = newTrackAttrs.operation === exports.CHANGE_OPERATION.insert
679
+ const mark = newTrackAttrs.operation === CHANGE_OPERATION.insert
662
680
  ? schema.marks.tracked_insert
663
681
  : schema.marks.tracked_delete;
664
682
  const createdMark = mark.create({
@@ -678,7 +696,7 @@ function recurseNodeContent(node, newTrackAttrs, schema) {
678
696
  return node.type.create({
679
697
  ...node.attrs,
680
698
  dataTracked: addTrackIdIfDoesntExist(newTrackAttrs),
681
- }, prosemirrorModel.Fragment.fromArray(updatedChildren), node.marks);
699
+ }, Fragment.fromArray(updatedChildren), node.marks);
682
700
  }
683
701
  else {
684
702
  log.error(`unhandled node type: "${node.type.name}"`, node);
@@ -691,7 +709,7 @@ function setFragmentAsInserted(inserted, insertAttrs, schema) {
691
709
  inserted.forEach((n) => {
692
710
  updatedInserted.push(recurseNodeContent(n, insertAttrs, schema));
693
711
  });
694
- return updatedInserted.length === 0 ? inserted : prosemirrorModel.Fragment.fromArray(updatedInserted);
712
+ return updatedInserted.length === 0 ? inserted : Fragment.fromArray(updatedInserted);
695
713
  }
696
714
 
697
715
  /*!
@@ -712,13 +730,13 @@ function setFragmentAsInserted(inserted, insertAttrs, schema) {
712
730
  function createNewInsertAttrs(attrs) {
713
731
  return {
714
732
  ...attrs,
715
- operation: exports.CHANGE_OPERATION.insert,
733
+ operation: CHANGE_OPERATION.insert,
716
734
  };
717
735
  }
718
736
  function createNewDeleteAttrs(attrs) {
719
737
  return {
720
738
  ...attrs,
721
- operation: exports.CHANGE_OPERATION.delete,
739
+ operation: CHANGE_OPERATION.delete,
722
740
  };
723
741
  }
724
742
 
@@ -761,7 +779,7 @@ function getMergedNode(node, currentDepth, depth, first) {
761
779
  };
762
780
  }
763
781
  const result = [];
764
- let merged = prosemirrorModel.Fragment.empty;
782
+ let merged = Fragment.empty;
765
783
  node.content.forEach((n, _, i) => {
766
784
  if ((first && i === 0) || (!first && i === node.childCount - 1)) {
767
785
  const { mergedNodeContent, unmergedContent } = getMergedNode(n, currentDepth + 1, depth, first);
@@ -776,7 +794,7 @@ function getMergedNode(node, currentDepth, depth, first) {
776
794
  });
777
795
  return {
778
796
  mergedNodeContent: merged,
779
- unmergedContent: result.length > 0 ? prosemirrorModel.Fragment.fromArray(result) : undefined,
797
+ unmergedContent: result.length > 0 ? Fragment.fromArray(result) : undefined,
780
798
  };
781
799
  }
782
800
  /**
@@ -839,7 +857,7 @@ function deleteTextIfInserted(node, pos, newTr, schema, deleteAttrs, from, to) {
839
857
  // Math.max(pos, from) is for picking always the start of the node,
840
858
  // not the start of the change (which might span multiple nodes).
841
859
  // Pos can be less than from as nodesBetween iterates through all nodes starting from the top block node
842
- newTr.replaceWith(start, end, prosemirrorModel.Fragment.empty);
860
+ newTr.replaceWith(start, end, Fragment.empty);
843
861
  return start;
844
862
  }
845
863
  else {
@@ -849,10 +867,12 @@ function deleteTextIfInserted(node, pos, newTr, schema, deleteAttrs, from, to) {
849
867
  const rightMarks = getMergeableMarkTrackedAttrs(rightNode, deleteAttrs, schema);
850
868
  const fromStartOfMark = start - (leftNode && leftMarks ? leftNode.nodeSize : 0);
851
869
  const toEndOfMark = end + (rightNode && rightMarks ? rightNode.nodeSize : 0);
870
+ 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);
852
871
  const dataTracked = addTrackIdIfDoesntExist({
853
872
  ...leftMarks,
854
873
  ...rightMarks,
855
874
  ...deleteAttrs,
875
+ createdAt,
856
876
  });
857
877
  newTr.addMark(fromStartOfMark, toEndOfMark, schema.marks.tracked_delete.create({
858
878
  dataTracked,
@@ -869,7 +889,8 @@ function deleteTextIfInserted(node, pos, newTr, schema, deleteAttrs, from, to) {
869
889
  */
870
890
  function deleteOrSetNodeDeleted(node, pos, newTr, deleteAttrs) {
871
891
  const dataTracked = node.attrs.dataTracked;
872
- const wasInsertedBySameUser = (dataTracked === null || dataTracked === void 0 ? void 0 : dataTracked.operation) === exports.CHANGE_OPERATION.insert && dataTracked.userID === deleteAttrs.userID;
892
+ const wasInsertedBySameUser = (dataTracked === null || dataTracked === void 0 ? void 0 : dataTracked.operation) === CHANGE_OPERATION.insert &&
893
+ dataTracked.authorID === deleteAttrs.authorID;
873
894
  if (wasInsertedBySameUser) {
874
895
  deleteNode(node, pos, newTr);
875
896
  }
@@ -907,7 +928,7 @@ function deleteOrSetNodeDeleted(node, pos, newTr, deleteAttrs) {
907
928
  * @returns mapping adjusted by the applied operations & modified insert slice
908
929
  */
909
930
  function deleteAndMergeSplitNodes(from, to, gap, startDoc, newTr, schema, trackAttrs, insertSlice) {
910
- const deleteMap = new prosemirrorTransform.Mapping();
931
+ const deleteMap = new Mapping();
911
932
  const mergedInsertPos = undefined;
912
933
  // No deletion applied, return default values
913
934
  if (from === to) {
@@ -925,10 +946,22 @@ function deleteAndMergeSplitNodes(from, to, gap, startDoc, newTr, schema, trackA
925
946
  const { pos: offsetPos, deleted: nodeWasDeleted } = deleteMap.mapResult(pos, 1);
926
947
  const offsetFrom = deleteMap.map(from, -1);
927
948
  const offsetTo = deleteMap.map(to, 1);
928
- const wasWithinGap = gap && offsetPos >= deleteMap.map(gap.start, -1);
929
949
  const nodeEnd = offsetPos + node.nodeSize;
950
+ // So this insane boolean checks for ReplaceAroundStep gaps and whether the node should be skipped
951
+ // since the content inside gap should stay unchanged.
952
+ // All other nodes except text nodes consist of one start and end token (or just a single token for atoms).
953
+ // For them we can just check whether the start token is within the gap eg pos is 10 when gap (8, 18) to
954
+ // determine whether it should be skipped.
955
+ // For text nodes though, since they are continous, they might only partially be enclosed in the gap
956
+ // eg. pos 10 when gap is (8, 18) BUT if their nodeEnd goes past the gap's end eg nodeEnd 20 they actually
957
+ // are altered and should not be skipped.
958
+ // @TODO ATM 20.7.2022 there doesn't seem to be tests that capture this.
959
+ const wasWithinGap = gap &&
960
+ ((!node.isText && offsetPos >= deleteMap.map(gap.start, -1)) ||
961
+ (node.isText &&
962
+ offsetPos <= deleteMap.map(gap.start, -1) &&
963
+ nodeEnd >= deleteMap.map(gap.end, -1)));
930
964
  let step = newTr.steps[newTr.steps.length - 1];
931
- // debugger
932
965
  // nodeEnd > offsetFrom -> delete touches this node
933
966
  // eg (del 6 10) <p 5>|<t 6>cdf</t 9></p 10>| -> <p> nodeEnd 10 > from 6
934
967
  //
@@ -937,6 +970,7 @@ function deleteAndMergeSplitNodes(from, to, gap, startDoc, newTr, schema, trackA
937
970
  // But from what I remember what it safeguards against is, when you've already deleted a node
938
971
  // say an inserted blockquote that had all its children deleted, nodesBetween still iterates over those
939
972
  // nodes and therefore we have to make this check to ensure they still exist in the doc.
973
+ //
940
974
  if (nodeEnd > offsetFrom && !nodeWasDeleted && !wasWithinGap) {
941
975
  // |<p>asdf</p>| -> node deleted completely
942
976
  const nodeCompletelyDeleted = offsetPos >= offsetFrom && nodeEnd <= offsetTo;
@@ -1029,7 +1063,7 @@ function deleteAndMergeSplitNodes(from, to, gap, startDoc, newTr, schema, trackA
1029
1063
  deleteMap,
1030
1064
  mergedInsertPos,
1031
1065
  newSliceContent: updatedSliceNodes
1032
- ? prosemirrorModel.Fragment.fromArray(updatedSliceNodes)
1066
+ ? Fragment.fromArray(updatedSliceNodes)
1033
1067
  : insertSlice.content,
1034
1068
  };
1035
1069
  }
@@ -1053,18 +1087,21 @@ function mergeTrackedMarks(pos, doc, newTr, schema) {
1053
1087
  if (!nodeAfter || !nodeBefore || !leftMark || !rightMark || leftMark.type !== rightMark.type) {
1054
1088
  return;
1055
1089
  }
1056
- const leftAttrs = leftMark.attrs;
1057
- const rightAttrs = rightMark.attrs;
1058
- if (!shouldMergeTrackedAttributes(leftAttrs.dataTracked, rightAttrs.dataTracked)) {
1090
+ const leftDataTracked = leftMark.attrs.dataTracked;
1091
+ const rightDataTracked = rightMark.attrs.dataTracked;
1092
+ if (!shouldMergeTrackedAttributes(leftDataTracked, rightDataTracked)) {
1059
1093
  return;
1060
1094
  }
1061
- const newAttrs = {
1062
- ...leftAttrs,
1063
- createdAt: Math.max(leftAttrs.createdAt || 0, rightAttrs.createdAt || 0) || Date.now(),
1095
+ const isLeftOlder = (leftDataTracked.createdAt || Number.MAX_VALUE) <
1096
+ (rightDataTracked.createdAt || Number.MAX_VALUE);
1097
+ const ancestorAttrs = isLeftOlder ? leftDataTracked : rightDataTracked;
1098
+ const dataTracked = {
1099
+ ...ancestorAttrs,
1100
+ updatedAt: Date.now(),
1064
1101
  };
1065
1102
  const fromStartOfMark = pos - nodeBefore.nodeSize;
1066
1103
  const toEndOfMark = pos + nodeAfter.nodeSize;
1067
- newTr.addMark(fromStartOfMark, toEndOfMark, leftMark.type.create(newAttrs));
1104
+ newTr.addMark(fromStartOfMark, toEndOfMark, leftMark.type.create({ ...leftMark.attrs, dataTracked }));
1068
1105
  }
1069
1106
 
1070
1107
  /*!
@@ -1128,13 +1165,13 @@ function trackReplaceAroundStep(step, oldState, newTr, attrs) {
1128
1165
  // the sides should be equal. TODO can they be other than 0?
1129
1166
  const openStart = slice.openStart !== slice.openEnd || newSliceContent.size === 0 ? 0 : slice.openStart;
1130
1167
  const openEnd = slice.openStart !== slice.openEnd || newSliceContent.size === 0 ? 0 : slice.openEnd;
1131
- let insertedSlice = new prosemirrorModel.Slice(setFragmentAsInserted(newSliceContent, createNewInsertAttrs(attrs), oldState.schema), openStart, openEnd);
1168
+ let insertedSlice = new Slice(setFragmentAsInserted(newSliceContent, createNewInsertAttrs(attrs), oldState.schema), openStart, openEnd);
1132
1169
  if (gap.size > 0) {
1133
1170
  log.info('insertedSlice before inserted gap', insertedSlice);
1134
1171
  insertedSlice = insertedSlice.insertAt(insertedSlice.size === 0 ? 0 : insert, gap.content);
1135
1172
  log.info('insertedSlice after inserted gap', insertedSlice);
1136
1173
  }
1137
- const newStep = new prosemirrorTransform.ReplaceStep(deleteMap.map(gapFrom), deleteMap.map(gapTo), insertedSlice, false);
1174
+ const newStep = new ReplaceStep(deleteMap.map(gapFrom), deleteMap.map(gapTo), insertedSlice, false);
1138
1175
  const stepResult = newTr.maybeStep(newStep);
1139
1176
  if (stepResult.failed) {
1140
1177
  log.error(`insert ReplaceStep failed: "${stepResult.failed}"`, newStep);
@@ -1179,10 +1216,11 @@ function trackReplaceStep(step, oldState, newTr, attrs) {
1179
1216
  log.error(`invert ReplaceStep failed: "${stepResult.failed}"`, newStep);
1180
1217
  return;
1181
1218
  }
1219
+ log.info('TR: steps before applying delete', [...newTr.steps]);
1182
1220
  // First apply the deleted range and update the insert slice to not include content that was deleted,
1183
1221
  // eg partial nodes in an open-ended slice
1184
1222
  const { deleteMap, mergedInsertPos, newSliceContent } = deleteAndMergeSplitNodes(fromA, toA, undefined, oldState.doc, newTr, oldState.schema, attrs, slice);
1185
- log.info('TR: new steps after applying delete', [...newTr.steps]);
1223
+ log.info('TR: steps after applying delete', [...newTr.steps]);
1186
1224
  const adjustedInsertPos = mergedInsertPos !== null && mergedInsertPos !== void 0 ? mergedInsertPos : deleteMap.map(toA);
1187
1225
  if (newSliceContent.size > 0) {
1188
1226
  log.info('newSliceContent', newSliceContent);
@@ -1190,8 +1228,8 @@ function trackReplaceStep(step, oldState, newTr, attrs) {
1190
1228
  // the sides should be equal. TODO can they be other than 0?
1191
1229
  const openStart = slice.openStart !== slice.openEnd ? 0 : slice.openStart;
1192
1230
  const openEnd = slice.openStart !== slice.openEnd ? 0 : slice.openEnd;
1193
- const insertedSlice = new prosemirrorModel.Slice(setFragmentAsInserted(newSliceContent, createNewInsertAttrs(attrs), oldState.schema), openStart, openEnd);
1194
- const newStep = new prosemirrorTransform.ReplaceStep(adjustedInsertPos, adjustedInsertPos, insertedSlice);
1231
+ const insertedSlice = new Slice(setFragmentAsInserted(newSliceContent, createNewInsertAttrs(attrs), oldState.schema), openStart, openEnd);
1232
+ const newStep = new ReplaceStep(adjustedInsertPos, adjustedInsertPos, insertedSlice);
1195
1233
  const stepResult = newTr.maybeStep(newStep);
1196
1234
  if (stepResult.failed) {
1197
1235
  log.error(`insert ReplaceStep failed: "${stepResult.failed}"`, newStep);
@@ -1217,11 +1255,9 @@ function trackReplaceStep(step, oldState, newTr, attrs) {
1217
1255
  * This skips the direct dependency to prosemirror-state where multiple versions might cause conflicts
1218
1256
  * as the created instances might belong to different prosemirror-state import than one used in the editor.
1219
1257
  * @param sel
1220
- * @param doc
1221
- * @param from
1222
1258
  * @returns
1223
1259
  */
1224
- const getSelectionStaticCreate = (sel, doc, from) => Object.getPrototypeOf(sel).constructor.create(doc, from);
1260
+ const getSelectionStaticConstructor = (sel) => Object.getPrototypeOf(sel).constructor;
1225
1261
  /**
1226
1262
  * Inverts transactions to wrap their contents/operations with track data instead
1227
1263
  *
@@ -1234,15 +1270,16 @@ const getSelectionStaticCreate = (sel, doc, from) => Object.getPrototypeOf(sel).
1234
1270
  * @param tr Original transaction
1235
1271
  * @param oldState State before transaction
1236
1272
  * @param newTr Transaction created from the new editor state
1237
- * @param userID User id
1273
+ * @param authorID User id
1238
1274
  * @returns newTr that inverts the initial tr and applies track attributes/marks
1239
1275
  */
1240
- function trackTransaction(tr, oldState, newTr, userID) {
1241
- var _a;
1276
+ function trackTransaction(tr, oldState, newTr, authorID) {
1242
1277
  const emptyAttrs = {
1243
- userID,
1278
+ authorID,
1279
+ reviewedByID: null,
1244
1280
  createdAt: tr.time,
1245
- status: exports.CHANGE_STATUS.pending,
1281
+ updatedAt: tr.time,
1282
+ status: CHANGE_STATUS.pending,
1246
1283
  };
1247
1284
  // Must use constructor.name instead of instanceof as aliasing prosemirror-state is a lot more
1248
1285
  // difficult than prosemirror-transform
@@ -1257,45 +1294,56 @@ function trackTransaction(tr, oldState, newTr, userID) {
1257
1294
  'This is probably an error with the library, please report back to maintainers with a reproduction if possible', newTr);
1258
1295
  return;
1259
1296
  }
1260
- else if (!(step instanceof prosemirrorTransform.ReplaceStep) && step.constructor.name === 'ReplaceStep') {
1297
+ else if (!(step instanceof ReplaceStep) && step.constructor.name === 'ReplaceStep') {
1261
1298
  console.error('@manuscripts/track-changes-plugin: Multiple prosemirror-transform packages imported, alias/dedupe them ' +
1262
1299
  'or instanceof checks fail as well as creating new steps');
1263
1300
  return;
1264
1301
  }
1265
- else if (step instanceof prosemirrorTransform.ReplaceStep) {
1302
+ else if (step instanceof ReplaceStep) {
1266
1303
  const selectionPos = trackReplaceStep(step, oldState, newTr, emptyAttrs);
1267
1304
  if (!wasNodeSelection) {
1268
- newTr.setSelection(getSelectionStaticCreate(tr.selection, newTr.doc, selectionPos));
1305
+ const sel = getSelectionStaticConstructor(tr.selection);
1306
+ // Use Selection.near to fix selections that point to a block node instead of inline content
1307
+ // eg when inserting a complete new paragraph. -1 finds the first valid position moving backwards
1308
+ // inside the content
1309
+ const near = sel.near(newTr.doc.resolve(selectionPos), -1);
1310
+ newTr.setSelection(near);
1269
1311
  }
1270
1312
  }
1271
- else if (step instanceof prosemirrorTransform.ReplaceAroundStep) {
1313
+ else if (step instanceof ReplaceAroundStep) {
1272
1314
  trackReplaceAroundStep(step, oldState, newTr, emptyAttrs);
1273
1315
  // } else if (step instanceof AddMarkStep) {
1274
1316
  // } else if (step instanceof RemoveMarkStep) {
1275
1317
  }
1318
+ // TODO: here we could check whether adjacent inserts & deletes cancel each other out.
1319
+ // However, this should not be done by diffing and only matching node or char by char instead since
1320
+ // it's A easier and B more intuitive to user.
1276
1321
  // The old meta keys are not copied to the new transaction since this will cause race-conditions
1277
- // when a single meta-field is thought to be processed. MAYBE only the generic meta keys, such as
1278
- // inputType or uiEvent, could be copied over but it remains to be seen if it's necessary.
1279
- // Object.keys(meta).forEach((key) => newTr.setMeta(key, tr.getMeta(key)))
1322
+ // when a single meta-field is expected to having been processed / removed. Generic input meta keys,
1323
+ // inputType and uiEvent, are re-added since some plugins might depend on them and process the transaction
1324
+ // after track-changes plugin.
1325
+ tr.getMeta('inputType') && newTr.setMeta('inputType', tr.getMeta('inputType'));
1326
+ tr.getMeta('uiEvent') && newTr.setMeta('uiEvent', tr.getMeta('uiEvent'));
1280
1327
  });
1281
1328
  // This is kinda hacky solution at the moment to maintain NodeSelections over transactions
1282
- // These are required by at least cross-references that need it to activate the selector pop-up
1329
+ // These are required by at least cross-references and links to activate their selector pop-ups
1283
1330
  if (wasNodeSelection) {
1284
- const mappedPos = newTr.mapping.map(tr.selection.from);
1285
- const resPos = newTr.doc.resolve(mappedPos);
1286
- const nodePos = mappedPos - (((_a = resPos.nodeBefore) === null || _a === void 0 ? void 0 : _a.nodeSize) || 0);
1287
- newTr.setSelection(getSelectionStaticCreate(tr.selection, newTr.doc, nodePos));
1331
+ // And -1 here is necessary to keep the selection pointing at the start of the node
1332
+ // (or something, breaks with cross-references otherwise)
1333
+ const mappedPos = newTr.mapping.map(tr.selection.from, -1);
1334
+ const sel = getSelectionStaticConstructor(tr.selection);
1335
+ newTr.setSelection(sel.create(newTr.doc, mappedPos));
1288
1336
  }
1289
1337
  log.info('NEW transaction', newTr);
1290
1338
  return newTr;
1291
1339
  }
1292
1340
 
1293
- exports.TrackChangesStatus = void 0;
1341
+ var TrackChangesStatus;
1294
1342
  (function (TrackChangesStatus) {
1295
1343
  TrackChangesStatus["enabled"] = "enabled";
1296
1344
  TrackChangesStatus["viewSnapshots"] = "view-snapshots";
1297
1345
  TrackChangesStatus["disabled"] = "disabled";
1298
- })(exports.TrackChangesStatus || (exports.TrackChangesStatus = {}));
1346
+ })(TrackChangesStatus || (TrackChangesStatus = {}));
1299
1347
 
1300
1348
  /*!
1301
1349
  * © 2021 Atypon Systems LLC
@@ -1312,12 +1360,7 @@ exports.TrackChangesStatus = void 0;
1312
1360
  * See the License for the specific language governing permissions and
1313
1361
  * limitations under the License.
1314
1362
  */
1315
- const trackChangesPluginKey = new prosemirrorState.PluginKey('track-changes');
1316
- // TODO remove
1317
- const infiniteLoopCounter = {
1318
- start: 0,
1319
- iters: 0,
1320
- };
1363
+ const trackChangesPluginKey = new PluginKey('track-changes');
1321
1364
  /**
1322
1365
  * The ProseMirror plugin needed to enable track-changes.
1323
1366
  *
@@ -1330,24 +1373,25 @@ const trackChangesPlugin = (opts = { userID: 'anonymous:Anonymous' }) => {
1330
1373
  if (debug) {
1331
1374
  enableDebug(true);
1332
1375
  }
1333
- return new prosemirrorState.Plugin({
1376
+ return new Plugin({
1334
1377
  key: trackChangesPluginKey,
1335
1378
  props: {
1336
1379
  editable(state) {
1337
- return this.getState(state).status !== exports.TrackChangesStatus.viewSnapshots;
1380
+ var _a;
1381
+ return ((_a = trackChangesPluginKey.getState(state)) === null || _a === void 0 ? void 0 : _a.status) !== TrackChangesStatus.viewSnapshots;
1338
1382
  },
1339
1383
  },
1340
1384
  state: {
1341
1385
  init(_config, state) {
1342
1386
  return {
1343
- status: exports.TrackChangesStatus.enabled,
1387
+ status: TrackChangesStatus.enabled,
1344
1388
  userID,
1345
1389
  changeSet: findChanges(state),
1346
1390
  };
1347
1391
  },
1348
1392
  apply(tr, pluginState, _oldState, newState) {
1349
- const setUserID = getAction(tr, exports.TrackChangesAction.setUserID);
1350
- const setStatus = getAction(tr, exports.TrackChangesAction.setPluginStatus);
1393
+ const setUserID = getAction(tr, TrackChangesAction.setUserID);
1394
+ const setStatus = getAction(tr, TrackChangesAction.setPluginStatus);
1351
1395
  if (setUserID) {
1352
1396
  return { ...pluginState, userID: setUserID };
1353
1397
  }
@@ -1358,12 +1402,12 @@ const trackChangesPlugin = (opts = { userID: 'anonymous:Anonymous' }) => {
1358
1402
  changeSet: findChanges(newState),
1359
1403
  };
1360
1404
  }
1361
- else if (pluginState.status === exports.TrackChangesStatus.disabled) {
1405
+ else if (pluginState.status === TrackChangesStatus.disabled) {
1362
1406
  return { ...pluginState, changeSet: new ChangeSet() };
1363
1407
  }
1364
1408
  let { changeSet, ...rest } = pluginState;
1365
- const updatedChangeIds = getAction(tr, exports.TrackChangesAction.updateChanges);
1366
- if (updatedChangeIds || getAction(tr, exports.TrackChangesAction.refreshChanges)) {
1409
+ const updatedChangeIds = getAction(tr, TrackChangesAction.updateChanges);
1410
+ if (updatedChangeIds || getAction(tr, TrackChangesAction.refreshChanges)) {
1367
1411
  changeSet = findChanges(newState);
1368
1412
  }
1369
1413
  return {
@@ -1382,47 +1426,37 @@ const trackChangesPlugin = (opts = { userID: 'anonymous:Anonymous' }) => {
1382
1426
  appendTransaction(trs, oldState, newState) {
1383
1427
  const pluginState = trackChangesPluginKey.getState(newState);
1384
1428
  if (!pluginState ||
1385
- pluginState.status === exports.TrackChangesStatus.disabled ||
1429
+ pluginState.status === TrackChangesStatus.disabled ||
1386
1430
  !(editorView === null || editorView === void 0 ? void 0 : editorView.editable)) {
1387
1431
  return null;
1388
1432
  }
1389
- if (infiniteLoopCounter.start < Date.now() - 10000) {
1390
- infiniteLoopCounter.start = Date.now();
1391
- infiniteLoopCounter.iters = 0;
1392
- }
1393
- if (infiniteLoopCounter.iters >= 100) {
1394
- console.error('Detected probable infinite loop in track changes!');
1395
- return null;
1396
- }
1397
1433
  const { userID, changeSet } = pluginState;
1398
1434
  let createdTr = newState.tr, docChanged = false;
1399
1435
  log.info('TRS', trs);
1400
1436
  trs.forEach((tr) => {
1401
1437
  const wasAppended = tr.getMeta('appendedTransaction');
1402
1438
  const skipMetaUsed = skipTrsWithMetas.some((m) => tr.getMeta(m) || (wasAppended === null || wasAppended === void 0 ? void 0 : wasAppended.getMeta(m)));
1403
- const skipTrackUsed = getAction(tr, exports.TrackChangesAction.skipTrack) ||
1404
- (wasAppended && getAction(wasAppended, exports.TrackChangesAction.skipTrack));
1439
+ const skipTrackUsed = getAction(tr, TrackChangesAction.skipTrack) ||
1440
+ (wasAppended && getAction(wasAppended, TrackChangesAction.skipTrack));
1405
1441
  if (tr.docChanged && !skipMetaUsed && !skipTrackUsed && !tr.getMeta('history$')) {
1406
1442
  createdTr = trackTransaction(tr, oldState, createdTr, userID);
1407
- createdTr.setMeta('origin', trackChangesPluginKey);
1408
- infiniteLoopCounter.iters += 1;
1409
1443
  }
1410
1444
  docChanged = docChanged || tr.docChanged;
1411
- const setChangeStatuses = getAction(tr, exports.TrackChangesAction.setChangeStatuses);
1445
+ const setChangeStatuses = getAction(tr, TrackChangesAction.setChangeStatuses);
1412
1446
  if (setChangeStatuses) {
1413
1447
  const { status, ids } = setChangeStatuses;
1414
1448
  ids.forEach((changeId) => {
1415
1449
  const change = changeSet === null || changeSet === void 0 ? void 0 : changeSet.get(changeId);
1416
1450
  if (change) {
1417
- createdTr = updateChangeAttrs(createdTr, change, { status }, oldState.schema);
1418
- setAction(createdTr, exports.TrackChangesAction.updateChanges, [change.id]);
1451
+ createdTr = updateChangeAttrs(createdTr, change, { status, reviewedByID: userID }, oldState.schema);
1452
+ setAction(createdTr, TrackChangesAction.updateChanges, [change.id]);
1419
1453
  }
1420
1454
  });
1421
1455
  }
1422
- else if (getAction(tr, exports.TrackChangesAction.applyAndRemoveChanges)) {
1456
+ else if (getAction(tr, TrackChangesAction.applyAndRemoveChanges)) {
1423
1457
  const mapping = applyAcceptedRejectedChanges(createdTr, oldState.schema, changeSet.nodeChanges);
1424
1458
  applyAcceptedRejectedChanges(createdTr, oldState.schema, changeSet.textChanges, mapping);
1425
- setAction(createdTr, exports.TrackChangesAction.refreshChanges, true);
1459
+ setAction(createdTr, TrackChangesAction.refreshChanges, true);
1426
1460
  }
1427
1461
  });
1428
1462
  const changed = pluginState.changeSet.hasInconsistentData &&
@@ -1431,7 +1465,8 @@ const trackChangesPlugin = (opts = { userID: 'anonymous:Anonymous' }) => {
1431
1465
  log.warn('had to fix inconsistent changes in', createdTr);
1432
1466
  }
1433
1467
  if (docChanged || createdTr.docChanged || changed) {
1434
- return setAction(createdTr, exports.TrackChangesAction.refreshChanges, true);
1468
+ createdTr.setMeta('origin', trackChangesPluginKey);
1469
+ return setAction(createdTr, TrackChangesAction.refreshChanges, true);
1435
1470
  }
1436
1471
  return null;
1437
1472
  },
@@ -1469,11 +1504,11 @@ const setTrackingStatus = (status) => (state, dispatch) => {
1469
1504
  let newStatus = status;
1470
1505
  if (newStatus === undefined) {
1471
1506
  newStatus =
1472
- currentStatus === exports.TrackChangesStatus.enabled
1473
- ? exports.TrackChangesStatus.disabled
1474
- : exports.TrackChangesStatus.enabled;
1507
+ currentStatus === TrackChangesStatus.enabled
1508
+ ? TrackChangesStatus.disabled
1509
+ : TrackChangesStatus.enabled;
1475
1510
  }
1476
- dispatch && dispatch(setAction(state.tr, exports.TrackChangesAction.setPluginStatus, newStatus));
1511
+ dispatch && dispatch(setAction(state.tr, TrackChangesAction.setPluginStatus, newStatus));
1477
1512
  return true;
1478
1513
  }
1479
1514
  return false;
@@ -1485,7 +1520,7 @@ const setTrackingStatus = (status) => (state, dispatch) => {
1485
1520
  */
1486
1521
  const setChangeStatuses = (status, ids) => (state, dispatch) => {
1487
1522
  dispatch &&
1488
- dispatch(setAction(state.tr, exports.TrackChangesAction.setChangeStatuses, {
1523
+ dispatch(setAction(state.tr, TrackChangesAction.setChangeStatuses, {
1489
1524
  status,
1490
1525
  ids,
1491
1526
  }));
@@ -1496,21 +1531,21 @@ const setChangeStatuses = (status, ids) => (state, dispatch) => {
1496
1531
  * @param userID
1497
1532
  */
1498
1533
  const setUserID = (userID) => (state, dispatch) => {
1499
- dispatch && dispatch(setAction(state.tr, exports.TrackChangesAction.setUserID, userID));
1534
+ dispatch && dispatch(setAction(state.tr, TrackChangesAction.setUserID, userID));
1500
1535
  return true;
1501
1536
  };
1502
1537
  /**
1503
1538
  * Appends a transaction that applies all 'accepted' and 'rejected' changes to the document.
1504
1539
  */
1505
1540
  const applyAndRemoveChanges = () => (state, dispatch) => {
1506
- dispatch && dispatch(setAction(state.tr, exports.TrackChangesAction.applyAndRemoveChanges, true));
1541
+ dispatch && dispatch(setAction(state.tr, TrackChangesAction.applyAndRemoveChanges, true));
1507
1542
  return true;
1508
1543
  };
1509
1544
  /**
1510
1545
  * Runs `findChanges` to iterate over the document to collect changes into a new ChangeSet.
1511
1546
  */
1512
1547
  const refreshChanges = () => (state, dispatch) => {
1513
- dispatch && dispatch(setAction(state.tr, exports.TrackChangesAction.updateChanges, []));
1548
+ dispatch && dispatch(setAction(state.tr, TrackChangesAction.updateChanges, []));
1514
1549
  return true;
1515
1550
  };
1516
1551
  /**
@@ -1540,10 +1575,4 @@ var commands = /*#__PURE__*/Object.freeze({
1540
1575
  setParagraphTestAttribute: setParagraphTestAttribute
1541
1576
  });
1542
1577
 
1543
- exports.ChangeSet = ChangeSet;
1544
- exports.enableDebug = enableDebug;
1545
- exports.getAction = getAction;
1546
- exports.setAction = setAction;
1547
- exports.trackChangesPlugin = trackChangesPlugin;
1548
- exports.trackChangesPluginKey = trackChangesPluginKey;
1549
- exports.trackCommands = commands;
1578
+ export { CHANGE_OPERATION, CHANGE_STATUS, ChangeSet, TrackChangesStatus, enableDebug, skipTracking, trackChangesPlugin, trackChangesPluginKey, commands as trackCommands };