@manuscripts/track-changes-plugin 0.1.1 → 0.4.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.
Files changed (29) hide show
  1. package/README.md +20 -45
  2. package/dist/ChangeSet.d.ts +3 -1
  3. package/dist/actions.d.ts +8 -1
  4. package/dist/change-steps/diffChangeSteps.d.ts +21 -0
  5. package/dist/change-steps/processChangeSteps.d.ts +21 -0
  6. package/dist/{track → changes}/applyChanges.d.ts +0 -0
  7. package/dist/{track → changes}/findChanges.d.ts +0 -0
  8. package/dist/{track → changes}/fixInconsistentChanges.d.ts +0 -0
  9. package/dist/{track → changes}/updateChangeAttrs.d.ts +0 -0
  10. package/dist/commands.d.ts +1 -1
  11. package/dist/{track/node-utils.d.ts → compute/nodeHelpers.d.ts} +4 -11
  12. package/dist/{track/steps → compute}/setFragmentAsInserted.d.ts +1 -1
  13. package/dist/compute/splitSliceIntoMergedParts.d.ts +41 -0
  14. package/dist/{index.es.js → index.cjs} +658 -380
  15. package/dist/index.d.ts +1 -1
  16. package/dist/index.js +663 -412
  17. package/dist/{track/steps → mutate}/deleteAndMergeSplitNodes.d.ts +4 -3
  18. package/dist/{track → mutate}/deleteNode.d.ts +9 -0
  19. package/dist/mutate/deleteText.d.ts +32 -0
  20. package/dist/{track → mutate}/mergeNode.d.ts +1 -1
  21. package/dist/{track/steps → mutate}/mergeTrackedMarks.d.ts +0 -0
  22. package/dist/{track/steps → steps}/trackReplaceAroundStep.d.ts +3 -2
  23. package/dist/{track/steps → steps}/trackReplaceStep.d.ts +3 -2
  24. package/dist/{track → steps}/trackTransaction.d.ts +2 -2
  25. package/dist/types/change.d.ts +22 -11
  26. package/dist/types/step.d.ts +52 -0
  27. package/dist/{track/steps → utils}/track-utils.d.ts +1 -2
  28. package/package.json +6 -6
  29. package/dist/types/editor.d.ts +0 -23
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,22 +82,22 @@ 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";
90
- CHANGE_OPERATION["set_node_attributes"] = "set_node_attributes";
91
- CHANGE_OPERATION["wrap_with_node"] = "wrap_with_node";
92
- CHANGE_OPERATION["unwrap_from_node"] = "unwrap_from_node";
93
- CHANGE_OPERATION["add_mark"] = "add_mark";
94
- CHANGE_OPERATION["remove_mark"] = "remove_mark";
95
- })(exports.CHANGE_OPERATION || (exports.CHANGE_OPERATION = {}));
96
- exports.CHANGE_STATUS = void 0;
89
+ CHANGE_OPERATION["set_node_attributes"] = "set_attrs";
90
+ // wrap_with_node = 'wrap_with_node',
91
+ // unwrap_from_node = 'unwrap_from_node',
92
+ // add_mark = 'add_mark',
93
+ // remove_mark = 'remove_mark',
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
 
@@ -192,16 +191,13 @@ class ChangeSet {
192
191
  rootNodes.push(currentNodeChange);
193
192
  currentNodeChange = undefined;
194
193
  }
195
- if (c.type === 'node-change' && currentNodeChange && c.from < currentNodeChange.to) {
194
+ if (currentNodeChange && c.from < currentNodeChange.to) {
196
195
  currentNodeChange.children.push(c);
197
196
  }
198
197
  else if (c.type === 'node-change') {
199
198
  currentNodeChange = { ...c, children: [] };
200
199
  }
201
- else if (c.type === 'text-change' && currentNodeChange && c.from < currentNodeChange.to) {
202
- currentNodeChange.children.push(c);
203
- }
204
- else if (c.type === 'text-change') {
200
+ else {
205
201
  rootNodes.push(c);
206
202
  }
207
203
  });
@@ -211,13 +207,13 @@ class ChangeSet {
211
207
  return rootNodes;
212
208
  }
213
209
  get pending() {
214
- return this.changeTree.filter((c) => c.attrs.status === exports.CHANGE_STATUS.pending);
210
+ return this.changeTree.filter((c) => c.attrs.status === CHANGE_STATUS.pending);
215
211
  }
216
212
  get accepted() {
217
- return this.changeTree.filter((c) => c.attrs.status === exports.CHANGE_STATUS.accepted);
213
+ return this.changeTree.filter((c) => c.attrs.status === CHANGE_STATUS.accepted);
218
214
  }
219
215
  get rejected() {
220
- return this.changeTree.filter((c) => c.attrs.status === exports.CHANGE_STATUS.rejected);
216
+ return this.changeTree.filter((c) => c.attrs.status === CHANGE_STATUS.rejected);
221
217
  }
222
218
  get textChanges() {
223
219
  return this.changes.filter((c) => c.type === 'text-change');
@@ -225,6 +221,9 @@ class ChangeSet {
225
221
  get nodeChanges() {
226
222
  return this.changes.filter((c) => c.type === 'node-change');
227
223
  }
224
+ get nodeAttrChanges() {
225
+ return this.changes.filter((c) => c.type === 'node-attr-change');
226
+ }
228
227
  get isEmpty() {
229
228
  return __classPrivateFieldGet(this, _ChangeSet_changes, "f").length === 0;
230
229
  }
@@ -270,9 +269,7 @@ class ChangeSet {
270
269
  * @param change
271
270
  */
272
271
  static shouldNotDelete(change) {
273
- 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));
272
+ return !ChangeSet.shouldDeleteChange(change);
276
273
  }
277
274
  /**
278
275
  * Determines whether a change should be deleted when applying it to the document.
@@ -280,8 +277,8 @@ class ChangeSet {
280
277
  */
281
278
  static shouldDeleteChange(change) {
282
279
  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));
280
+ return ((operation === CHANGE_OPERATION.insert && status === CHANGE_STATUS.rejected) ||
281
+ (operation === CHANGE_OPERATION.delete && status === CHANGE_STATUS.accepted));
285
282
  }
286
283
  /**
287
284
  * Checks whether change attributes contain all TrackedAttrs keys with non-undefined values
@@ -291,10 +288,22 @@ class ChangeSet {
291
288
  if ('attrs' in attrs) {
292
289
  log.warn('passed "attrs" as property to isValidTrackedAttrs(attrs)', attrs);
293
290
  }
294
- const trackedKeys = ['id', 'userID', 'operation', 'status', 'createdAt'];
295
- const entries = Object.entries(attrs);
291
+ const trackedKeys = [
292
+ 'id',
293
+ 'authorID',
294
+ 'operation',
295
+ 'status',
296
+ 'createdAt',
297
+ 'updatedAt',
298
+ ];
299
+ // reviewedByID is set optional since either ProseMirror or Yjs doesn't like persisting null values inside attributes objects
300
+ // So it can be either omitted completely or at least be null or string
301
+ const optionalKeys = ['reviewedByID'];
302
+ const entries = Object.entries(attrs).filter(([key, val]) => trackedKeys.includes(key));
303
+ const optionalEntries = Object.entries(attrs).filter(([key, val]) => optionalKeys.includes(key));
296
304
  return (entries.length === trackedKeys.length &&
297
305
  entries.every(([key, val]) => trackedKeys.includes(key) && val !== undefined) &&
306
+ optionalEntries.every(([key, val]) => optionalKeys.includes(key) && val !== undefined) &&
298
307
  (attrs.id || '').length > 0 // Changes created with undefined id have '' as placeholder
299
308
  );
300
309
  }
@@ -304,9 +313,89 @@ class ChangeSet {
304
313
  static isNodeChange(change) {
305
314
  return change.type === 'node-change';
306
315
  }
316
+ static isNodeAttrChange(change) {
317
+ return change.type === 'node-attr-change';
318
+ }
307
319
  }
308
320
  _ChangeSet_changes = new WeakMap();
309
321
 
322
+ /*!
323
+ * © 2021 Atypon Systems LLC
324
+ *
325
+ * Licensed under the Apache License, Version 2.0 (the "License");
326
+ * you may not use this file except in compliance with the License.
327
+ * You may obtain a copy of the License at
328
+ *
329
+ * http://www.apache.org/licenses/LICENSE-2.0
330
+ *
331
+ * Unless required by applicable law or agreed to in writing, software
332
+ * distributed under the License is distributed on an "AS IS" BASIS,
333
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
334
+ * See the License for the specific language governing permissions and
335
+ * limitations under the License.
336
+ */
337
+ function uuidv4() {
338
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
339
+ const r = (Math.random() * 16) | 0, v = c == 'x' ? r : (r & 0x3) | 0x8;
340
+ return v.toString(16);
341
+ });
342
+ }
343
+
344
+ function addTrackIdIfDoesntExist(attrs) {
345
+ if (!attrs.id) {
346
+ return {
347
+ id: uuidv4(),
348
+ ...attrs,
349
+ };
350
+ }
351
+ return attrs;
352
+ }
353
+ function getInlineNodeTrackedMarkData(node, schema) {
354
+ if (!node || !node.isInline) {
355
+ return undefined;
356
+ }
357
+ const marksTrackedData = [];
358
+ node.marks.forEach((mark) => {
359
+ if (mark.type === schema.marks.tracked_insert || mark.type === schema.marks.tracked_delete) {
360
+ const operation = mark.type === schema.marks.tracked_insert
361
+ ? CHANGE_OPERATION.insert
362
+ : CHANGE_OPERATION.delete;
363
+ marksTrackedData.push({ ...mark.attrs.dataTracked, operation });
364
+ }
365
+ });
366
+ if (marksTrackedData.length > 1) {
367
+ log.warn('inline node with more than 1 of tracked marks', marksTrackedData);
368
+ }
369
+ return marksTrackedData[0] || undefined;
370
+ }
371
+ function getNodeTrackedData(node, schema) {
372
+ return !node
373
+ ? undefined
374
+ : node.isText
375
+ ? getInlineNodeTrackedMarkData(node, schema)
376
+ : node.attrs.dataTracked;
377
+ }
378
+ function equalMarks(n1, n2) {
379
+ return (n1.marks.length === n2.marks.length &&
380
+ n1.marks.every((mark) => n1.marks.find((m) => m.type === mark.type)));
381
+ }
382
+ function shouldMergeTrackedAttributes(left, right) {
383
+ if (!left || !right) {
384
+ log.warn('passed undefined dataTracked attributes to shouldMergeTrackedAttributes', {
385
+ left,
386
+ right,
387
+ });
388
+ return false;
389
+ }
390
+ return (left.status === right.status &&
391
+ left.operation === right.operation &&
392
+ left.authorID === right.authorID);
393
+ }
394
+ function getMergeableMarkTrackedAttrs(node, attrs, schema) {
395
+ const nodeAttrs = getInlineNodeTrackedMarkData(node, schema);
396
+ return nodeAttrs && shouldMergeTrackedAttributes(nodeAttrs, attrs) ? nodeAttrs : null;
397
+ }
398
+
310
399
  /*!
311
400
  * © 2021 Atypon Systems LLC
312
401
  *
@@ -335,15 +424,17 @@ function deleteNode(node, pos, tr) {
335
424
  var _a;
336
425
  const startPos = tr.doc.resolve(pos + 1);
337
426
  const range = startPos.blockRange(tr.doc.resolve(startPos.pos - 2 + node.nodeSize));
338
- const targetDepth = range && prosemirrorTransform.liftTarget(range);
339
- // Check with typeof since with old prosemirror-transform targetDepth is undefined
427
+ const targetDepth = range && liftTarget(range);
428
+ // Check with typeof since with prosemirror-transform pre 1.6.0 targetDepth is undefined
340
429
  if (range && typeof targetDepth === 'number') {
341
430
  return tr.lift(range, targetDepth);
342
431
  }
343
432
  const resPos = tr.doc.resolve(pos);
344
- const canMergeToNodeAbove = (resPos.parent !== tr.doc || resPos.nodeBefore) && ((_a = node.firstChild) === null || _a === void 0 ? void 0 : _a.isText);
433
+ // Block nodes can be deleted by just removing their start token which should then merge the text
434
+ // content to above node's content (if there is one)
435
+ const canMergeToNodeAbove = (resPos.parent !== tr.doc || resPos.nodeBefore) && node.isBlock && ((_a = node.firstChild) === null || _a === void 0 ? void 0 : _a.isText);
345
436
  if (canMergeToNodeAbove) {
346
- return tr.replaceWith(pos - 1, pos + 1, prosemirrorModel.Fragment.empty);
437
+ return tr.replaceWith(pos - 1, pos + 1, Fragment.empty);
347
438
  }
348
439
  else {
349
440
  // NOTE: there's an edge case where moving content is not possible but because the immediate
@@ -376,98 +467,22 @@ function deleteNode(node, pos, tr) {
376
467
  */
377
468
  function mergeNode(node, pos, tr) {
378
469
  var _a;
379
- if (prosemirrorTransform.canJoin(tr.doc, pos)) {
470
+ if (canJoin(tr.doc, pos)) {
380
471
  return tr.join(pos);
381
472
  }
382
- else if (prosemirrorTransform.canJoin(tr.doc, pos + node.nodeSize)) {
473
+ else if (canJoin(tr.doc, pos + node.nodeSize)) {
474
+ // TODO should copy the attributes from the merged node below
383
475
  return tr.join(pos + node.nodeSize);
384
476
  }
385
- // TODO is this the same thing as join?
477
+ // TODO is this the same thing as join to above?
386
478
  const resPos = tr.doc.resolve(pos);
387
479
  const canMergeToNodeAbove = (resPos.parent !== tr.doc || resPos.nodeBefore) && ((_a = node.firstChild) === null || _a === void 0 ? void 0 : _a.isText);
388
480
  if (canMergeToNodeAbove) {
389
- return tr.replaceWith(pos - 1, pos + 1, prosemirrorModel.Fragment.empty);
481
+ return tr.replaceWith(pos - 1, pos + 1, Fragment.empty);
390
482
  }
391
483
  return undefined;
392
484
  }
393
485
 
394
- /*!
395
- * © 2021 Atypon Systems LLC
396
- *
397
- * Licensed under the Apache License, Version 2.0 (the "License");
398
- * you may not use this file except in compliance with the License.
399
- * You may obtain a copy of the License at
400
- *
401
- * http://www.apache.org/licenses/LICENSE-2.0
402
- *
403
- * Unless required by applicable law or agreed to in writing, software
404
- * distributed under the License is distributed on an "AS IS" BASIS,
405
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
406
- * See the License for the specific language governing permissions and
407
- * limitations under the License.
408
- */
409
- function uuidv4() {
410
- return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
411
- const r = (Math.random() * 16) | 0, v = c == 'x' ? r : (r & 0x3) | 0x8;
412
- return v.toString(16);
413
- });
414
- }
415
-
416
- function addTrackIdIfDoesntExist(attrs) {
417
- if (!attrs.id) {
418
- return {
419
- id: uuidv4(),
420
- ...attrs,
421
- };
422
- }
423
- return attrs;
424
- }
425
- function getInlineNodeTrackedMarkData(node, schema) {
426
- if (!node || !node.isInline) {
427
- return undefined;
428
- }
429
- const marksTrackedData = [];
430
- node.marks.forEach((mark) => {
431
- if (mark.type === schema.marks.tracked_insert || mark.type === schema.marks.tracked_delete) {
432
- const operation = mark.type === schema.marks.tracked_insert
433
- ? exports.CHANGE_OPERATION.insert
434
- : exports.CHANGE_OPERATION.delete;
435
- marksTrackedData.push({ ...mark.attrs.dataTracked, operation });
436
- }
437
- });
438
- if (marksTrackedData.length > 1) {
439
- log.warn('inline node with more than 1 of tracked marks', marksTrackedData);
440
- }
441
- return marksTrackedData[0] || undefined;
442
- }
443
- function getNodeTrackedData(node, schema) {
444
- return !node
445
- ? undefined
446
- : node.isText
447
- ? getInlineNodeTrackedMarkData(node, schema)
448
- : node.attrs.dataTracked;
449
- }
450
- function equalMarks(n1, n2) {
451
- return (n1.marks.length === n2.marks.length &&
452
- n1.marks.every((mark) => n1.marks.find((m) => m.type === mark.type)));
453
- }
454
- function shouldMergeTrackedAttributes(left, right) {
455
- if (!left || !right) {
456
- log.warn('passed undefined dataTracked attributes to shouldMergeTrackedAttributes', {
457
- left,
458
- right,
459
- });
460
- return false;
461
- }
462
- return (left.status === right.status &&
463
- left.operation === right.operation &&
464
- left.userID === right.userID);
465
- }
466
- function getMergeableMarkTrackedAttrs(node, attrs, schema) {
467
- const nodeAttrs = getInlineNodeTrackedMarkData(node, schema);
468
- return nodeAttrs && shouldMergeTrackedAttributes(nodeAttrs, attrs) ? nodeAttrs : null;
469
- }
470
-
471
486
  function updateChangeAttrs(tr, change, trackedAttrs, schema) {
472
487
  const node = tr.doc.nodeAt(change.from);
473
488
  if (!node) {
@@ -505,9 +520,9 @@ function updateChangeChildrenAttributes(changes, tr, mapping) {
505
520
  * @param changes
506
521
  * @param deleteMap
507
522
  */
508
- function applyAcceptedRejectedChanges(tr, schema, changes, deleteMap = new prosemirrorTransform.Mapping()) {
523
+ function applyAcceptedRejectedChanges(tr, schema, changes, deleteMap = new Mapping()) {
509
524
  changes.forEach((change) => {
510
- if (change.attrs.status === exports.CHANGE_STATUS.pending) {
525
+ if (change.attrs.status === CHANGE_STATUS.pending) {
511
526
  return;
512
527
  }
513
528
  // Map change.from and skip those which dont need to be applied
@@ -540,6 +555,16 @@ function applyAcceptedRejectedChanges(tr, schema, changes, deleteMap = new prose
540
555
  }
541
556
  deleteMap.appendMap(tr.steps[tr.steps.length - 1].getMap());
542
557
  }
558
+ else if (ChangeSet.isNodeAttrChange(change) &&
559
+ change.attrs.status === CHANGE_STATUS.accepted) {
560
+ const attrs = { ...node.attrs, dataTracked: null };
561
+ tr.setNodeMarkup(from, undefined, attrs, node.marks);
562
+ }
563
+ else if (ChangeSet.isNodeAttrChange(change) &&
564
+ change.attrs.status === CHANGE_STATUS.rejected) {
565
+ const attrs = { ...change.oldAttrs, dataTracked: null };
566
+ tr.setNodeMarkup(from, undefined, attrs, node.marks);
567
+ }
543
568
  });
544
569
  return deleteMap;
545
570
  }
@@ -581,6 +606,7 @@ function findChanges(state) {
581
606
  type: 'text-change',
582
607
  from: pos,
583
608
  to: pos + node.nodeSize,
609
+ text: node.text,
584
610
  attrs,
585
611
  },
586
612
  node,
@@ -625,13 +651,15 @@ function fixInconsistentChanges(changeSet, trackUserID, newTr, schema) {
625
651
  const iteratedIds = new Set();
626
652
  let changed = false;
627
653
  changeSet.invalidChanges.forEach((c) => {
628
- const { id, userID, operation, status, createdAt } = c.attrs;
654
+ const { id, authorID, operation, reviewedByID, status, createdAt, updatedAt } = c.attrs;
629
655
  const newAttrs = {
630
656
  ...((!id || iteratedIds.has(id) || id.length === 0) && { id: uuidv4() }),
631
- ...(!userID && { userID: trackUserID }),
632
- ...(!operation && { operation: exports.CHANGE_OPERATION.insert }),
633
- ...(!status && { status: exports.CHANGE_STATUS.pending }),
657
+ ...(!authorID && { authorID: trackUserID }),
658
+ ...(!operation && { operation: CHANGE_OPERATION.insert }),
659
+ ...(!reviewedByID && { reviewedByID: null }),
660
+ ...(!status && { status: CHANGE_STATUS.pending }),
634
661
  ...(!createdAt && { createdAt: Date.now() }),
662
+ ...(!updatedAt && { updatedAt: Date.now() }),
635
663
  };
636
664
  if (Object.keys(newAttrs).length > 0) {
637
665
  updateChangeAttrs(newTr, c, { ...c.attrs, ...newAttrs }, schema);
@@ -642,87 +670,6 @@ function fixInconsistentChanges(changeSet, trackUserID, newTr, schema) {
642
670
  return changed;
643
671
  }
644
672
 
645
- /*!
646
- * © 2021 Atypon Systems LLC
647
- *
648
- * Licensed under the Apache License, Version 2.0 (the "License");
649
- * you may not use this file except in compliance with the License.
650
- * You may obtain a copy of the License at
651
- *
652
- * http://www.apache.org/licenses/LICENSE-2.0
653
- *
654
- * Unless required by applicable law or agreed to in writing, software
655
- * distributed under the License is distributed on an "AS IS" BASIS,
656
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
657
- * See the License for the specific language governing permissions and
658
- * limitations under the License.
659
- */
660
- function markInlineNodeChange(node, newTrackAttrs, schema) {
661
- const filtered = node.marks.filter((m) => m.type !== schema.marks.tracked_insert && m.type !== schema.marks.tracked_delete);
662
- const mark = newTrackAttrs.operation === exports.CHANGE_OPERATION.insert
663
- ? schema.marks.tracked_insert
664
- : schema.marks.tracked_delete;
665
- const createdMark = mark.create({
666
- dataTracked: addTrackIdIfDoesntExist(newTrackAttrs),
667
- });
668
- return node.mark(filtered.concat(createdMark));
669
- }
670
- function recurseNodeContent(node, newTrackAttrs, schema) {
671
- if (node.isText) {
672
- return markInlineNodeChange(node, newTrackAttrs, schema);
673
- }
674
- else if (node.isBlock || node.isInline) {
675
- const updatedChildren = [];
676
- node.content.forEach((child) => {
677
- updatedChildren.push(recurseNodeContent(child, newTrackAttrs, schema));
678
- });
679
- return node.type.create({
680
- ...node.attrs,
681
- dataTracked: addTrackIdIfDoesntExist(newTrackAttrs),
682
- }, prosemirrorModel.Fragment.fromArray(updatedChildren), node.marks);
683
- }
684
- else {
685
- log.error(`unhandled node type: "${node.type.name}"`, node);
686
- return node;
687
- }
688
- }
689
- function setFragmentAsInserted(inserted, insertAttrs, schema) {
690
- // Recurse the content in the inserted slice and either mark it tracked_insert or set node attrs
691
- const updatedInserted = [];
692
- inserted.forEach((n) => {
693
- updatedInserted.push(recurseNodeContent(n, insertAttrs, schema));
694
- });
695
- return updatedInserted.length === 0 ? inserted : prosemirrorModel.Fragment.fromArray(updatedInserted);
696
- }
697
-
698
- /*!
699
- * © 2021 Atypon Systems LLC
700
- *
701
- * Licensed under the Apache License, Version 2.0 (the "License");
702
- * you may not use this file except in compliance with the License.
703
- * You may obtain a copy of the License at
704
- *
705
- * http://www.apache.org/licenses/LICENSE-2.0
706
- *
707
- * Unless required by applicable law or agreed to in writing, software
708
- * distributed under the License is distributed on an "AS IS" BASIS,
709
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
710
- * See the License for the specific language governing permissions and
711
- * limitations under the License.
712
- */
713
- function createNewInsertAttrs(attrs) {
714
- return {
715
- ...attrs,
716
- operation: exports.CHANGE_OPERATION.insert,
717
- };
718
- }
719
- function createNewDeleteAttrs(attrs) {
720
- return {
721
- ...attrs,
722
- operation: exports.CHANGE_OPERATION.delete,
723
- };
724
- }
725
-
726
673
  /*!
727
674
  * © 2021 Atypon Systems LLC
728
675
  *
@@ -762,7 +709,7 @@ function getMergedNode(node, currentDepth, depth, first) {
762
709
  };
763
710
  }
764
711
  const result = [];
765
- let merged = prosemirrorModel.Fragment.empty;
712
+ let merged = Fragment.empty;
766
713
  node.content.forEach((n, _, i) => {
767
714
  if ((first && i === 0) || (!first && i === node.childCount - 1)) {
768
715
  const { mergedNodeContent, unmergedContent } = getMergedNode(n, currentDepth + 1, depth, first);
@@ -777,7 +724,7 @@ function getMergedNode(node, currentDepth, depth, first) {
777
724
  });
778
725
  return {
779
726
  mergedNodeContent: merged,
780
- unmergedContent: result.length > 0 ? prosemirrorModel.Fragment.fromArray(result) : undefined,
727
+ unmergedContent: result.length > 0 ? Fragment.fromArray(result) : undefined,
781
728
  };
782
729
  }
783
730
  /**
@@ -818,70 +765,104 @@ function splitSliceIntoMergedParts(insertSlice, mergeEqualSides = false) {
818
765
  firstMergedNode,
819
766
  lastMergedNode,
820
767
  };
821
- }
822
- /**
823
- * Deletes inserted text directly, otherwise wraps it with tracked_delete mark
768
+ }
769
+
770
+ /*!
771
+ * © 2021 Atypon Systems LLC
824
772
  *
825
- * This would work for general inline nodes too, but since node marks don't work properly
826
- * with Yjs, attributes are used instead.
827
- * @param node
828
- * @param pos
829
- * @param newTr
830
- * @param schema
831
- * @param deleteAttrs
832
- * @param from
833
- * @param to
773
+ * Licensed under the Apache License, Version 2.0 (the "License");
774
+ * you may not use this file except in compliance with the License.
775
+ * You may obtain a copy of the License at
776
+ *
777
+ * http://www.apache.org/licenses/LICENSE-2.0
778
+ *
779
+ * Unless required by applicable law or agreed to in writing, software
780
+ * distributed under the License is distributed on an "AS IS" BASIS,
781
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
782
+ * See the License for the specific language governing permissions and
783
+ * limitations under the License.
834
784
  */
835
- function deleteTextIfInserted(node, pos, newTr, schema, deleteAttrs, from, to) {
836
- const start = from ? Math.max(pos, from) : pos;
837
- const nodeEnd = pos + node.nodeSize;
838
- const end = to ? Math.min(nodeEnd, to) : nodeEnd;
839
- if (node.marks.find((m) => m.type === schema.marks.tracked_insert)) {
840
- // Math.max(pos, from) is for picking always the start of the node,
841
- // not the start of the change (which might span multiple nodes).
842
- // Pos can be less than from as nodesBetween iterates through all nodes starting from the top block node
843
- newTr.replaceWith(start, end, prosemirrorModel.Fragment.empty);
844
- return start;
785
+ function markInlineNodeChange(node, newTrackAttrs, schema) {
786
+ const filtered = node.marks.filter((m) => m.type !== schema.marks.tracked_insert && m.type !== schema.marks.tracked_delete);
787
+ const mark = newTrackAttrs.operation === CHANGE_OPERATION.insert
788
+ ? schema.marks.tracked_insert
789
+ : schema.marks.tracked_delete;
790
+ const createdMark = mark.create({
791
+ dataTracked: addTrackIdIfDoesntExist(newTrackAttrs),
792
+ });
793
+ return node.mark(filtered.concat(createdMark));
794
+ }
795
+ function recurseNodeContent(node, newTrackAttrs, schema) {
796
+ if (node.isText) {
797
+ return markInlineNodeChange(node, newTrackAttrs, schema);
845
798
  }
846
- else {
847
- const leftNode = newTr.doc.resolve(start).nodeBefore;
848
- const leftMarks = getMergeableMarkTrackedAttrs(leftNode, deleteAttrs, schema);
849
- const rightNode = newTr.doc.resolve(end).nodeAfter;
850
- const rightMarks = getMergeableMarkTrackedAttrs(rightNode, deleteAttrs, schema);
851
- const fromStartOfMark = start - (leftNode && leftMarks ? leftNode.nodeSize : 0);
852
- const toEndOfMark = end + (rightNode && rightMarks ? rightNode.nodeSize : 0);
853
- const dataTracked = addTrackIdIfDoesntExist({
854
- ...leftMarks,
855
- ...rightMarks,
856
- ...deleteAttrs,
799
+ else if (node.isBlock || node.isInline) {
800
+ const updatedChildren = [];
801
+ node.content.forEach((child) => {
802
+ updatedChildren.push(recurseNodeContent(child, newTrackAttrs, schema));
857
803
  });
858
- newTr.addMark(fromStartOfMark, toEndOfMark, schema.marks.tracked_delete.create({
859
- dataTracked,
860
- }));
861
- return toEndOfMark;
862
- }
863
- }
864
- /**
865
- * Deletes inserted block or inline node, otherwise adds `dataTracked` object with CHANGE_STATUS 'deleted'
866
- * @param node
867
- * @param pos
868
- * @param newTr
869
- * @param deleteAttrs
870
- */
871
- function deleteOrSetNodeDeleted(node, pos, newTr, deleteAttrs) {
872
- const dataTracked = node.attrs.dataTracked;
873
- const wasInsertedBySameUser = (dataTracked === null || dataTracked === void 0 ? void 0 : dataTracked.operation) === exports.CHANGE_OPERATION.insert && dataTracked.userID === deleteAttrs.userID;
874
- if (wasInsertedBySameUser) {
875
- deleteNode(node, pos, newTr);
804
+ return node.type.create({
805
+ ...node.attrs,
806
+ dataTracked: addTrackIdIfDoesntExist(newTrackAttrs),
807
+ }, Fragment.fromArray(updatedChildren), node.marks);
876
808
  }
877
809
  else {
878
- const attrs = {
879
- ...node.attrs,
880
- dataTracked: addTrackIdIfDoesntExist(deleteAttrs),
881
- };
882
- newTr.setNodeMarkup(pos, undefined, attrs, node.marks);
810
+ log.error(`unhandled node type: "${node.type.name}"`, node);
811
+ return node;
883
812
  }
884
813
  }
814
+ function setFragmentAsInserted(inserted, insertAttrs, schema) {
815
+ // Recurse the content in the inserted slice and either mark it tracked_insert or set node attrs
816
+ const updatedInserted = [];
817
+ inserted.forEach((n) => {
818
+ updatedInserted.push(recurseNodeContent(n, insertAttrs, schema));
819
+ });
820
+ return updatedInserted.length === 0 ? inserted : Fragment.fromArray(updatedInserted);
821
+ }
822
+
823
+ /*!
824
+ * © 2021 Atypon Systems LLC
825
+ *
826
+ * Licensed under the Apache License, Version 2.0 (the "License");
827
+ * you may not use this file except in compliance with the License.
828
+ * You may obtain a copy of the License at
829
+ *
830
+ * http://www.apache.org/licenses/LICENSE-2.0
831
+ *
832
+ * Unless required by applicable law or agreed to in writing, software
833
+ * distributed under the License is distributed on an "AS IS" BASIS,
834
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
835
+ * See the License for the specific language governing permissions and
836
+ * limitations under the License.
837
+ */
838
+ function createNewInsertAttrs(attrs) {
839
+ return {
840
+ ...attrs,
841
+ operation: CHANGE_OPERATION.insert,
842
+ };
843
+ }
844
+ function createNewDeleteAttrs(attrs) {
845
+ return {
846
+ ...attrs,
847
+ operation: CHANGE_OPERATION.delete,
848
+ };
849
+ }
850
+
851
+ /*!
852
+ * © 2021 Atypon Systems LLC
853
+ *
854
+ * Licensed under the Apache License, Version 2.0 (the "License");
855
+ * you may not use this file except in compliance with the License.
856
+ * You may obtain a copy of the License at
857
+ *
858
+ * http://www.apache.org/licenses/LICENSE-2.0
859
+ *
860
+ * Unless required by applicable law or agreed to in writing, software
861
+ * distributed under the License is distributed on an "AS IS" BASIS,
862
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
863
+ * See the License for the specific language governing permissions and
864
+ * limitations under the License.
865
+ */
885
866
  /**
886
867
  * Applies deletion to the doc without actually deleting nodes that have not been inserted
887
868
  *
@@ -908,28 +889,39 @@ function deleteOrSetNodeDeleted(node, pos, newTr, deleteAttrs) {
908
889
  * @returns mapping adjusted by the applied operations & modified insert slice
909
890
  */
910
891
  function deleteAndMergeSplitNodes(from, to, gap, startDoc, newTr, schema, trackAttrs, insertSlice) {
911
- const deleteMap = new prosemirrorTransform.Mapping();
912
- const mergedInsertPos = undefined;
892
+ const deleteMap = new Mapping();
893
+ const steps = [];
913
894
  // No deletion applied, return default values
914
895
  if (from === to) {
915
896
  return {
916
897
  deleteMap,
917
- mergedInsertPos,
918
898
  newSliceContent: insertSlice.content,
899
+ steps,
919
900
  };
920
901
  }
921
902
  const { openStart, openEnd } = insertSlice;
922
903
  const { updatedSliceNodes, firstMergedNode, lastMergedNode } = splitSliceIntoMergedParts(insertSlice, gap !== undefined);
923
- const deleteAttrs = createNewDeleteAttrs(trackAttrs);
924
904
  let mergingStartSide = true;
925
905
  startDoc.nodesBetween(from, to, (node, pos) => {
926
906
  const { pos: offsetPos, deleted: nodeWasDeleted } = deleteMap.mapResult(pos, 1);
927
907
  const offsetFrom = deleteMap.map(from, -1);
928
908
  const offsetTo = deleteMap.map(to, 1);
929
- const wasWithinGap = gap && offsetPos >= deleteMap.map(gap.start, -1);
930
909
  const nodeEnd = offsetPos + node.nodeSize;
910
+ // So this insane boolean checks for ReplaceAroundStep gaps and whether the node should be skipped
911
+ // since the content inside gap should stay unchanged.
912
+ // All other nodes except text nodes consist of one start and end token (or just a single token for atoms).
913
+ // For them we can just check whether the start token is within the gap eg pos is 10 when gap (8, 18) to
914
+ // determine whether it should be skipped.
915
+ // For text nodes though, since they are continous, they might only partially be enclosed in the gap
916
+ // eg. pos 10 when gap is (8, 18) BUT if their nodeEnd goes past the gap's end eg nodeEnd 20 they actually
917
+ // are altered and should not be skipped.
918
+ // @TODO ATM 20.7.2022 there doesn't seem to be tests that capture this.
919
+ const wasWithinGap = gap &&
920
+ ((!node.isText && offsetPos >= deleteMap.map(gap.start, -1)) ||
921
+ (node.isText &&
922
+ offsetPos <= deleteMap.map(gap.start, -1) &&
923
+ nodeEnd >= deleteMap.map(gap.end, -1)));
931
924
  let step = newTr.steps[newTr.steps.length - 1];
932
- // debugger
933
925
  // nodeEnd > offsetFrom -> delete touches this node
934
926
  // eg (del 6 10) <p 5>|<t 6>cdf</t 9></p 10>| -> <p> nodeEnd 10 > from 6
935
927
  //
@@ -984,24 +976,18 @@ function deleteAndMergeSplitNodes(from, to, gap, startDoc, newTr, schema, trackA
984
976
  // ProseMirror node semantics as start tokens are considered to contain the actual node itself.
985
977
  const mergeEndNode = startTokenDeleted && openEnd > 0 && depth === openEnd && mergeContent !== undefined;
986
978
  if (mergeStartNode || mergeEndNode) {
987
- // The default insert position for block nodes is either the start of the merged content or the end.
988
- // Incase text was merged, this must be updated as the start or end of the node doesn't map to the
989
- // actual position of the merge. Currently the inserted content is inserted at the start or end
990
- // of the merged content, TODO reverse the start/end when end/start token?
991
- let insertPos = mergeStartNode ? nodeEnd - openStart : offsetPos + openEnd;
992
- if (node.isText) {
993
- // When merging text we must delete text in the same go as well, as the from/to boundary goes through
994
- // the text node.
995
- insertPos = deleteTextIfInserted(node, offsetPos, newTr, schema, deleteAttrs, offsetFrom, offsetTo);
996
- deleteMap.appendMap(newTr.steps[newTr.steps.length - 1].getMap());
997
- step = newTr.steps[newTr.steps.length - 1];
998
- }
999
979
  // Just as a fun fact that I found out while debugging this. Inserting text at paragraph position wraps
1000
980
  // it into a new paragraph(!). So that's why you always offset your positions to insert it _inside_
1001
981
  // the paragraph.
1002
- if (mergeContent.size !== 0) {
1003
- newTr.insert(insertPos, setFragmentAsInserted(mergeContent, createNewInsertAttrs(trackAttrs), schema));
1004
- }
982
+ steps.push({
983
+ type: 'merge-fragment',
984
+ pos: offsetPos,
985
+ mergePos: mergeStartNode ? nodeEnd - openStart : offsetPos + openEnd,
986
+ from: offsetFrom,
987
+ to: offsetTo,
988
+ node,
989
+ fragment: setFragmentAsInserted(mergeContent, createNewInsertAttrs(trackAttrs), schema),
990
+ });
1005
991
  // Okay this is a bit ridiculous but it's used to adjust the insert pos when track changes prevents deletions
1006
992
  // of merged nodes & content, as just using mapped toA in that case isn't the same.
1007
993
  // The calculation is a bit mysterious, I admit.
@@ -1014,12 +1000,23 @@ function deleteAndMergeSplitNodes(from, to, gap, startDoc, newTr, schema, trackA
1014
1000
  else if (node.isText) {
1015
1001
  // Text deletion is handled even when the deletion doesn't completely wrap the text node
1016
1002
  // (which is basically the case most of the time)
1017
- deleteTextIfInserted(node, offsetPos, newTr, schema, deleteAttrs, offsetFrom, offsetTo);
1003
+ steps.push({
1004
+ type: 'delete-text',
1005
+ pos: offsetPos,
1006
+ from: Math.max(offsetPos, offsetFrom),
1007
+ to: Math.min(nodeEnd, offsetTo),
1008
+ node,
1009
+ });
1018
1010
  }
1019
1011
  else ;
1020
1012
  }
1021
1013
  else if (nodeCompletelyDeleted) {
1022
- deleteOrSetNodeDeleted(node, offsetPos, newTr, deleteAttrs);
1014
+ steps.push({
1015
+ type: 'delete-node',
1016
+ pos: offsetPos,
1017
+ nodeEnd: nodeEnd,
1018
+ node,
1019
+ });
1023
1020
  }
1024
1021
  }
1025
1022
  const newestStep = newTr.steps[newTr.steps.length - 1];
@@ -1029,10 +1026,10 @@ function deleteAndMergeSplitNodes(from, to, gap, startDoc, newTr, schema, trackA
1029
1026
  });
1030
1027
  return {
1031
1028
  deleteMap,
1032
- mergedInsertPos,
1033
1029
  newSliceContent: updatedSliceNodes
1034
- ? prosemirrorModel.Fragment.fromArray(updatedSliceNodes)
1030
+ ? Fragment.fromArray(updatedSliceNodes)
1035
1031
  : insertSlice.content,
1032
+ steps,
1036
1033
  };
1037
1034
  }
1038
1035
 
@@ -1055,18 +1052,20 @@ function mergeTrackedMarks(pos, doc, newTr, schema) {
1055
1052
  if (!nodeAfter || !nodeBefore || !leftMark || !rightMark || leftMark.type !== rightMark.type) {
1056
1053
  return;
1057
1054
  }
1058
- const leftAttrs = leftMark.attrs;
1059
- const rightAttrs = rightMark.attrs;
1060
- if (!shouldMergeTrackedAttributes(leftAttrs.dataTracked, rightAttrs.dataTracked)) {
1055
+ const leftDataTracked = leftMark.attrs.dataTracked;
1056
+ const rightDataTracked = rightMark.attrs.dataTracked;
1057
+ if (!shouldMergeTrackedAttributes(leftDataTracked, rightDataTracked)) {
1061
1058
  return;
1062
1059
  }
1063
- const newAttrs = {
1064
- ...leftAttrs,
1065
- createdAt: Math.max(leftAttrs.createdAt || 0, rightAttrs.createdAt || 0) || Date.now(),
1060
+ const isLeftOlder = (leftDataTracked.createdAt || 0) < (rightDataTracked.createdAt || 0);
1061
+ const ancestorAttrs = isLeftOlder ? leftDataTracked : rightDataTracked;
1062
+ const dataTracked = {
1063
+ ...ancestorAttrs,
1064
+ updatedAt: Date.now(),
1066
1065
  };
1067
1066
  const fromStartOfMark = pos - nodeBefore.nodeSize;
1068
1067
  const toEndOfMark = pos + nodeAfter.nodeSize;
1069
- newTr.addMark(fromStartOfMark, toEndOfMark, leftMark.type.create(newAttrs));
1068
+ newTr.addMark(fromStartOfMark, toEndOfMark, leftMark.type.create({ ...leftMark.attrs, dataTracked }));
1070
1069
  }
1071
1070
 
1072
1071
  /*!
@@ -1114,14 +1113,16 @@ function trackReplaceAroundStep(step, oldState, newTr, attrs) {
1114
1113
  const stepResult = newTr.maybeStep(newStep);
1115
1114
  if (stepResult.failed) {
1116
1115
  log.error(`inverting ReplaceAroundStep failed: "${stepResult.failed}"`, newStep);
1117
- return;
1116
+ return [];
1118
1117
  }
1119
1118
  const gap = oldState.doc.slice(gapFrom, gapTo);
1120
1119
  log.info('RETAINED GAP CONTENT', gap);
1121
1120
  // First apply the deleted range and update the insert slice to not include content that was deleted,
1122
1121
  // eg partial nodes in an open-ended slice
1123
- const { deleteMap, newSliceContent } = deleteAndMergeSplitNodes(from, to, { start: gapFrom, end: gapTo }, newTr.doc, newTr, oldState.schema, attrs, slice);
1122
+ const { deleteMap, newSliceContent, steps: deleteSteps, } = deleteAndMergeSplitNodes(from, to, { start: gapFrom, end: gapTo }, newTr.doc, newTr, oldState.schema, attrs, slice);
1123
+ let steps = deleteSteps;
1124
1124
  log.info('TR: new steps after applying delete', [...newTr.steps]);
1125
+ log.info('DELETE STEPS: ', deleteSteps);
1125
1126
  // We only want to insert when there something inside the gap (actually would this be always true?)
1126
1127
  // or insert slice wasn't just start/end tokens (which we already merged inside deleteAndMergeSplitBlockNodes)
1127
1128
  if (gap.size > 0 || (!structure && newSliceContent.size > 0)) {
@@ -1130,27 +1131,25 @@ function trackReplaceAroundStep(step, oldState, newTr, attrs) {
1130
1131
  // the sides should be equal. TODO can they be other than 0?
1131
1132
  const openStart = slice.openStart !== slice.openEnd || newSliceContent.size === 0 ? 0 : slice.openStart;
1132
1133
  const openEnd = slice.openStart !== slice.openEnd || newSliceContent.size === 0 ? 0 : slice.openEnd;
1133
- let insertedSlice = new prosemirrorModel.Slice(setFragmentAsInserted(newSliceContent, createNewInsertAttrs(attrs), oldState.schema), openStart, openEnd);
1134
+ let insertedSlice = new Slice(setFragmentAsInserted(newSliceContent, createNewInsertAttrs(attrs), oldState.schema), openStart, openEnd);
1134
1135
  if (gap.size > 0) {
1135
1136
  log.info('insertedSlice before inserted gap', insertedSlice);
1136
1137
  insertedSlice = insertedSlice.insertAt(insertedSlice.size === 0 ? 0 : insert, gap.content);
1137
1138
  log.info('insertedSlice after inserted gap', insertedSlice);
1138
1139
  }
1139
- const newStep = new prosemirrorTransform.ReplaceStep(deleteMap.map(gapFrom), deleteMap.map(gapTo), insertedSlice, false);
1140
- const stepResult = newTr.maybeStep(newStep);
1141
- if (stepResult.failed) {
1142
- log.error(`insert ReplaceStep failed: "${stepResult.failed}"`, newStep);
1143
- return;
1144
- }
1145
- log.info('new steps after applying insert', [...newTr.steps]);
1146
- mergeTrackedMarks(deleteMap.map(gapFrom), newTr.doc, newTr, oldState.schema);
1147
- mergeTrackedMarks(deleteMap.map(gapTo), newTr.doc, newTr, oldState.schema);
1140
+ deleteSteps.push({
1141
+ type: 'insert-slice',
1142
+ from: deleteMap.map(gapFrom),
1143
+ to: deleteMap.map(gapTo),
1144
+ slice: insertedSlice,
1145
+ });
1148
1146
  }
1149
1147
  else {
1150
1148
  // Incase only deletion was applied, check whether tracked marks around deleted content can be merged
1151
1149
  mergeTrackedMarks(deleteMap.map(gapFrom), newTr.doc, newTr, oldState.schema);
1152
1150
  mergeTrackedMarks(deleteMap.map(gapTo), newTr.doc, newTr, oldState.schema);
1153
1151
  }
1152
+ return steps;
1154
1153
  }
1155
1154
 
1156
1155
  /*!
@@ -1170,7 +1169,7 @@ function trackReplaceAroundStep(step, oldState, newTr, attrs) {
1170
1169
  */
1171
1170
  function trackReplaceStep(step, oldState, newTr, attrs) {
1172
1171
  log.info('###### ReplaceStep ######');
1173
- let selectionPos = 0;
1172
+ let selectionPos = 0, changeSteps = [];
1174
1173
  step.getMap().forEach((fromA, toA, fromB, toB) => {
1175
1174
  log.info(`changed ranges: ${fromA} ${toA} ${fromB} ${toB}`);
1176
1175
  const { slice } = step;
@@ -1181,36 +1180,309 @@ function trackReplaceStep(step, oldState, newTr, attrs) {
1181
1180
  log.error(`invert ReplaceStep failed: "${stepResult.failed}"`, newStep);
1182
1181
  return;
1183
1182
  }
1183
+ log.info('TR: steps before applying delete', [...newTr.steps]);
1184
1184
  // First apply the deleted range and update the insert slice to not include content that was deleted,
1185
1185
  // eg partial nodes in an open-ended slice
1186
- const { deleteMap, mergedInsertPos, newSliceContent } = deleteAndMergeSplitNodes(fromA, toA, undefined, oldState.doc, newTr, oldState.schema, attrs, slice);
1187
- log.info('TR: new steps after applying delete', [...newTr.steps]);
1188
- const adjustedInsertPos = mergedInsertPos !== null && mergedInsertPos !== void 0 ? mergedInsertPos : deleteMap.map(toA);
1186
+ const { deleteMap, newSliceContent, steps: deleteSteps, } = deleteAndMergeSplitNodes(fromA, toA, undefined, oldState.doc, newTr, oldState.schema, attrs, slice);
1187
+ changeSteps.push(...deleteSteps);
1188
+ log.info('TR: steps after applying delete', [...newTr.steps]);
1189
+ log.info('DELETE STEPS: ', changeSteps);
1190
+ const adjustedInsertPos = deleteMap.map(toA);
1189
1191
  if (newSliceContent.size > 0) {
1190
1192
  log.info('newSliceContent', newSliceContent);
1191
1193
  // Since deleteAndMergeSplitBlockNodes modified the slice to not to contain any merged nodes,
1192
1194
  // the sides should be equal. TODO can they be other than 0?
1193
1195
  const openStart = slice.openStart !== slice.openEnd ? 0 : slice.openStart;
1194
1196
  const openEnd = slice.openStart !== slice.openEnd ? 0 : slice.openEnd;
1195
- const insertedSlice = new prosemirrorModel.Slice(setFragmentAsInserted(newSliceContent, createNewInsertAttrs(attrs), oldState.schema), openStart, openEnd);
1196
- const newStep = new prosemirrorTransform.ReplaceStep(adjustedInsertPos, adjustedInsertPos, insertedSlice);
1197
+ changeSteps.push({
1198
+ type: 'insert-slice',
1199
+ from: adjustedInsertPos,
1200
+ to: adjustedInsertPos,
1201
+ slice: new Slice(setFragmentAsInserted(newSliceContent, createNewInsertAttrs(attrs), oldState.schema), openStart, openEnd),
1202
+ });
1203
+ }
1204
+ else {
1205
+ // Incase only deletion was applied, check whether tracked marks around deleted content can be merged
1206
+ mergeTrackedMarks(adjustedInsertPos, newTr.doc, newTr, oldState.schema);
1207
+ selectionPos = fromA;
1208
+ }
1209
+ });
1210
+ return [changeSteps, selectionPos];
1211
+ }
1212
+
1213
+ /*!
1214
+ * © 2021 Atypon Systems LLC
1215
+ *
1216
+ * Licensed under the Apache License, Version 2.0 (the "License");
1217
+ * you may not use this file except in compliance with the License.
1218
+ * You may obtain a copy of the License at
1219
+ *
1220
+ * http://www.apache.org/licenses/LICENSE-2.0
1221
+ *
1222
+ * Unless required by applicable law or agreed to in writing, software
1223
+ * distributed under the License is distributed on an "AS IS" BASIS,
1224
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1225
+ * See the License for the specific language governing permissions and
1226
+ * limitations under the License.
1227
+ */
1228
+ /**
1229
+ * Deletes inserted text directly, otherwise wraps it with tracked_delete mark
1230
+ *
1231
+ * This would work for general inline nodes too, but since node marks don't work properly
1232
+ * with Yjs, attributes are used instead.
1233
+ * @param node
1234
+ * @param pos
1235
+ * @param newTr
1236
+ * @param schema
1237
+ * @param deleteAttrs
1238
+ * @param from
1239
+ * @param to
1240
+ */
1241
+ function deleteTextIfInserted(node, pos, newTr, schema, deleteAttrs, from, to) {
1242
+ const start = from ? Math.max(pos, from) : pos;
1243
+ const nodeEnd = pos + node.nodeSize;
1244
+ const end = to ? Math.min(nodeEnd, to) : nodeEnd;
1245
+ if (node.marks.find((m) => m.type === schema.marks.tracked_insert)) {
1246
+ // Math.max(pos, from) is for picking always the start of the node,
1247
+ // not the start of the change (which might span multiple nodes).
1248
+ // Pos can be less than from as nodesBetween iterates through all nodes starting from the top block node
1249
+ newTr.replaceWith(start, end, Fragment.empty);
1250
+ return start;
1251
+ }
1252
+ else {
1253
+ const leftNode = newTr.doc.resolve(start).nodeBefore;
1254
+ const leftMarks = getMergeableMarkTrackedAttrs(leftNode, deleteAttrs, schema);
1255
+ const rightNode = newTr.doc.resolve(end).nodeAfter;
1256
+ const rightMarks = getMergeableMarkTrackedAttrs(rightNode, deleteAttrs, schema);
1257
+ const fromStartOfMark = start - (leftNode && leftMarks ? leftNode.nodeSize : 0);
1258
+ const toEndOfMark = end + (rightNode && rightMarks ? rightNode.nodeSize : 0);
1259
+ 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);
1260
+ const dataTracked = addTrackIdIfDoesntExist({
1261
+ ...leftMarks,
1262
+ ...rightMarks,
1263
+ ...deleteAttrs,
1264
+ createdAt,
1265
+ });
1266
+ newTr.addMark(fromStartOfMark, toEndOfMark, schema.marks.tracked_delete.create({
1267
+ dataTracked,
1268
+ }));
1269
+ return toEndOfMark;
1270
+ }
1271
+ }
1272
+
1273
+ /*!
1274
+ * © 2021 Atypon Systems LLC
1275
+ *
1276
+ * Licensed under the Apache License, Version 2.0 (the "License");
1277
+ * you may not use this file except in compliance with the License.
1278
+ * You may obtain a copy of the License at
1279
+ *
1280
+ * http://www.apache.org/licenses/LICENSE-2.0
1281
+ *
1282
+ * Unless required by applicable law or agreed to in writing, software
1283
+ * distributed under the License is distributed on an "AS IS" BASIS,
1284
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1285
+ * See the License for the specific language governing permissions and
1286
+ * limitations under the License.
1287
+ */
1288
+ function processChangeSteps(changes, startPos, newTr, emptyAttrs, schema) {
1289
+ const mapping = new Mapping();
1290
+ const deleteAttrs = createNewDeleteAttrs(emptyAttrs);
1291
+ let selectionPos = startPos;
1292
+ // @TODO add custom handler / condition?
1293
+ changes.forEach((c) => {
1294
+ let step = newTr.steps[newTr.steps.length - 1];
1295
+ log.info('process change: ', c);
1296
+ // const handled = customStepHandler(changes, newTr, emptyAttrs) // ChangeStep[] | undefined
1297
+ if (c.type === 'delete-node') {
1298
+ const dataTracked = c.node.attrs.dataTracked;
1299
+ const wasInsertedBySameUser = (dataTracked === null || dataTracked === void 0 ? void 0 : dataTracked.operation) === CHANGE_OPERATION.insert &&
1300
+ dataTracked.authorID === emptyAttrs.authorID;
1301
+ if (wasInsertedBySameUser) {
1302
+ deleteNode(c.node, mapping.map(c.pos), newTr);
1303
+ const newestStep = newTr.steps[newTr.steps.length - 1];
1304
+ if (step !== newestStep) {
1305
+ mapping.appendMap(newestStep.getMap());
1306
+ step = newestStep;
1307
+ }
1308
+ mergeTrackedMarks(mapping.map(c.pos), newTr.doc, newTr, schema);
1309
+ }
1310
+ else {
1311
+ const attrs = {
1312
+ ...c.node.attrs,
1313
+ dataTracked: addTrackIdIfDoesntExist(deleteAttrs),
1314
+ };
1315
+ newTr.setNodeMarkup(mapping.map(c.pos), undefined, attrs, c.node.marks);
1316
+ }
1317
+ }
1318
+ else if (c.type === 'delete-text') {
1319
+ const from = mapping.map(c.from, -1);
1320
+ const to = mapping.map(c.to, 1);
1321
+ const node = newTr.doc.nodeAt(mapping.map(c.pos));
1322
+ if (node === null || node === void 0 ? void 0 : node.marks.find((m) => m.type === schema.marks.tracked_insert)) {
1323
+ newTr.replaceWith(from, to, Fragment.empty);
1324
+ mergeTrackedMarks(from, newTr.doc, newTr, schema);
1325
+ }
1326
+ else {
1327
+ const leftNode = newTr.doc.resolve(from).nodeBefore;
1328
+ const leftMarks = getMergeableMarkTrackedAttrs(leftNode, deleteAttrs, schema);
1329
+ const rightNode = newTr.doc.resolve(to).nodeAfter;
1330
+ const rightMarks = getMergeableMarkTrackedAttrs(rightNode, deleteAttrs, schema);
1331
+ const fromStartOfMark = from - (leftNode && leftMarks ? leftNode.nodeSize : 0);
1332
+ const toEndOfMark = to + (rightNode && rightMarks ? rightNode.nodeSize : 0);
1333
+ const dataTracked = addTrackIdIfDoesntExist({
1334
+ ...leftMarks,
1335
+ ...rightMarks,
1336
+ ...deleteAttrs,
1337
+ });
1338
+ newTr.addMark(fromStartOfMark, toEndOfMark, schema.marks.tracked_delete.create({
1339
+ dataTracked,
1340
+ }));
1341
+ }
1342
+ }
1343
+ else if (c.type === 'merge-fragment') {
1344
+ let insertPos = mapping.map(c.mergePos);
1345
+ // The default insert position for block nodes is either the start of the merged content or the end.
1346
+ // Incase text was merged, this must be updated as the start or end of the node doesn't map to the
1347
+ // actual position of the merge. Currently the inserted content is inserted at the start or end
1348
+ // of the merged content, TODO reverse the start/end when end/start token?
1349
+ if (c.node.isText) {
1350
+ // When merging text we must delete text in the same go as well, as the from/to boundary goes through
1351
+ // the text node.
1352
+ insertPos = deleteTextIfInserted(c.node, mapping.map(c.pos), newTr, schema, deleteAttrs, mapping.map(c.from), mapping.map(c.to));
1353
+ const newestStep = newTr.steps[newTr.steps.length - 1];
1354
+ if (step !== newestStep) {
1355
+ mapping.appendMap(newestStep.getMap());
1356
+ step = newestStep;
1357
+ }
1358
+ }
1359
+ if (c.fragment.size > 0) {
1360
+ newTr.insert(insertPos, c.fragment);
1361
+ }
1362
+ }
1363
+ else if (c.type === 'insert-slice') {
1364
+ const newStep = new ReplaceStep(mapping.map(c.from), mapping.map(c.to), c.slice, false);
1197
1365
  const stepResult = newTr.maybeStep(newStep);
1198
1366
  if (stepResult.failed) {
1199
1367
  log.error(`insert ReplaceStep failed: "${stepResult.failed}"`, newStep);
1200
1368
  return;
1201
1369
  }
1202
- log.info('new steps after applying insert', [...newTr.steps]);
1203
- mergeTrackedMarks(adjustedInsertPos, newTr.doc, newTr, oldState.schema);
1204
- mergeTrackedMarks(adjustedInsertPos + insertedSlice.size, newTr.doc, newTr, oldState.schema);
1205
- selectionPos = adjustedInsertPos + insertedSlice.size;
1370
+ mergeTrackedMarks(mapping.map(c.from), newTr.doc, newTr, schema);
1371
+ mergeTrackedMarks(mapping.map(c.to), newTr.doc, newTr, schema);
1372
+ selectionPos = mapping.map(c.to) + c.slice.size;
1206
1373
  }
1207
- else {
1208
- // Incase only deletion was applied, check whether tracked marks around deleted content can be merged
1209
- mergeTrackedMarks(adjustedInsertPos, newTr.doc, newTr, oldState.schema);
1210
- selectionPos = fromA;
1374
+ else if (c.type === 'update-node-attrs') {
1375
+ const oldDataTracked = c.oldAttrs.dataTracked;
1376
+ const oldAttrs = (oldDataTracked === null || oldDataTracked === void 0 ? void 0 : oldDataTracked.operation) === CHANGE_OPERATION.set_node_attributes
1377
+ ? oldDataTracked.oldAttrs
1378
+ : c.oldAttrs;
1379
+ const dataTracked = addTrackIdIfDoesntExist({
1380
+ ...oldDataTracked,
1381
+ oldAttrs,
1382
+ ...emptyAttrs,
1383
+ operation: CHANGE_OPERATION.set_node_attributes,
1384
+ });
1385
+ newTr.setNodeMarkup(mapping.map(c.pos), undefined, { ...c.newAttrs, dataTracked });
1386
+ }
1387
+ const newestStep = newTr.steps[newTr.steps.length - 1];
1388
+ if (step !== newestStep) {
1389
+ mapping.appendMap(newestStep.getMap());
1390
+ }
1391
+ });
1392
+ return [mapping, selectionPos];
1393
+ }
1394
+
1395
+ /*!
1396
+ * © 2021 Atypon Systems LLC
1397
+ *
1398
+ * Licensed under the Apache License, Version 2.0 (the "License");
1399
+ * you may not use this file except in compliance with the License.
1400
+ * You may obtain a copy of the License at
1401
+ *
1402
+ * http://www.apache.org/licenses/LICENSE-2.0
1403
+ *
1404
+ * Unless required by applicable law or agreed to in writing, software
1405
+ * distributed under the License is distributed on an "AS IS" BASIS,
1406
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1407
+ * See the License for the specific language governing permissions and
1408
+ * limitations under the License.
1409
+ */
1410
+ function matchInserted(inDeleted, deleted, inserted, newTr, schema) {
1411
+ var _a;
1412
+ for (let i = 0;; i += 1) {
1413
+ if (inserted.childCount === i)
1414
+ return [inDeleted, deleted];
1415
+ const child = inserted.child(i);
1416
+ // @ts-ignore
1417
+ let adjDeleted = deleted.find((d) => (d.type === 'delete-text' && d.to === inDeleted) ||
1418
+ (d.type === 'delete-node' && d.nodeEnd === inDeleted));
1419
+ if (child.type !== ((_a = adjDeleted === null || adjDeleted === void 0 ? void 0 : adjDeleted.node) === null || _a === void 0 ? void 0 : _a.type)) {
1420
+ return [inDeleted, deleted];
1421
+ }
1422
+ else if (child.isText && (adjDeleted === null || adjDeleted === void 0 ? void 0 : adjDeleted.node)) {
1423
+ adjDeleted = adjDeleted;
1424
+ const { pos, from, to, node } = adjDeleted;
1425
+ let j = 0, d = from - pos, maxSteps = Math.max(pos, from) - to;
1426
+ // Match text inside the inserted text node to the deleted text node
1427
+ for (; maxSteps !== j && child.text[j] === node.text[d]; j += 1, d += 1) {
1428
+ inDeleted -= 1;
1429
+ }
1430
+ // this is needed incase diffing tr.doc
1431
+ // deleted.push({
1432
+ // pos: pos,
1433
+ // type: 'update-node-attrs',
1434
+ // // Should check the attrs for equality in fixInconsistentChanges? to remove dataTracked completely
1435
+ // oldAttrs: adjDeleted.node.attrs || {},
1436
+ // newAttrs: child.attrs || {},
1437
+ // })
1438
+ if (maxSteps !== j) {
1439
+ deleted.push({
1440
+ pos,
1441
+ from: Math.max(pos, from) + j,
1442
+ to,
1443
+ type: 'delete-text',
1444
+ node,
1445
+ });
1446
+ }
1447
+ return [inDeleted, deleted.filter((d) => d !== adjDeleted)];
1448
+ }
1449
+ else if (child.content.size > 0 || (adjDeleted === null || adjDeleted === void 0 ? void 0 : adjDeleted.node.content.size) > 0) {
1450
+ // Move the inDeleted inside the block node's boundary
1451
+ return matchInserted(inDeleted - 1, deleted.filter((d) => d !== adjDeleted), child.content);
1452
+ }
1453
+ deleted.push({
1454
+ pos: adjDeleted.pos,
1455
+ type: 'update-node-attrs',
1456
+ // Should check the attrs for equality in fixInconsistentChanges? to remove dataTracked completely
1457
+ oldAttrs: adjDeleted.node.attrs || {},
1458
+ newAttrs: child.attrs || {},
1459
+ });
1460
+ deleted = deleted.filter((d) => d !== adjDeleted);
1461
+ inDeleted -= child.nodeSize;
1462
+ }
1463
+ }
1464
+ function diffChangeSteps(deleted, inserted, newTr, schema) {
1465
+ const updated = [];
1466
+ let updatedDeleted = [...deleted];
1467
+ inserted.forEach((ins) => {
1468
+ const [inDeleted, updatedDel] = matchInserted(ins.from, updatedDeleted, ins.slice.content);
1469
+ if (inDeleted === ins.from) {
1470
+ updated.push(ins);
1471
+ return;
1472
+ }
1473
+ updatedDeleted = updatedDel;
1474
+ const newInsertedA = ins.slice.content.cut(ins.from - inDeleted);
1475
+ const newInsertedB = ins.slice.content.cut(ins.from - inDeleted + 1);
1476
+ // Super hax to cut over block node boundaries in the inserted fragment
1477
+ const newInserted = newInsertedA.size === newInsertedB.size + 2 ? newInsertedB : newInsertedA;
1478
+ if (newInserted.size > 0) {
1479
+ updated.push({
1480
+ ...ins,
1481
+ slice: new Slice(newInserted, ins.slice.openStart, ins.slice.openEnd),
1482
+ });
1211
1483
  }
1212
1484
  });
1213
- return selectionPos;
1485
+ return [...updatedDeleted, ...updated];
1214
1486
  }
1215
1487
 
1216
1488
  /**
@@ -1234,15 +1506,16 @@ const getSelectionStaticConstructor = (sel) => Object.getPrototypeOf(sel).constr
1234
1506
  * @param tr Original transaction
1235
1507
  * @param oldState State before transaction
1236
1508
  * @param newTr Transaction created from the new editor state
1237
- * @param userID User id
1509
+ * @param authorID User id
1238
1510
  * @returns newTr that inverts the initial tr and applies track attributes/marks
1239
1511
  */
1240
- function trackTransaction(tr, oldState, newTr, userID) {
1241
- var _a;
1512
+ function trackTransaction(tr, oldState, newTr, authorID) {
1242
1513
  const emptyAttrs = {
1243
- userID,
1514
+ authorID,
1515
+ reviewedByID: null,
1244
1516
  createdAt: tr.time,
1245
- status: exports.CHANGE_STATUS.pending,
1517
+ updatedAt: tr.time,
1518
+ status: CHANGE_STATUS.pending,
1246
1519
  };
1247
1520
  // Must use constructor.name instead of instanceof as aliasing prosemirror-state is a lot more
1248
1521
  // difficult than prosemirror-transform
@@ -1257,13 +1530,20 @@ function trackTransaction(tr, oldState, newTr, userID) {
1257
1530
  'This is probably an error with the library, please report back to maintainers with a reproduction if possible', newTr);
1258
1531
  return;
1259
1532
  }
1260
- else if (!(step instanceof prosemirrorTransform.ReplaceStep) && step.constructor.name === 'ReplaceStep') {
1533
+ else if (!(step instanceof ReplaceStep) && step.constructor.name === 'ReplaceStep') {
1261
1534
  console.error('@manuscripts/track-changes-plugin: Multiple prosemirror-transform packages imported, alias/dedupe them ' +
1262
1535
  'or instanceof checks fail as well as creating new steps');
1263
1536
  return;
1264
1537
  }
1265
- else if (step instanceof prosemirrorTransform.ReplaceStep) {
1266
- const selectionPos = trackReplaceStep(step, oldState, newTr, emptyAttrs);
1538
+ else if (step instanceof ReplaceStep) {
1539
+ let [steps, startPos] = trackReplaceStep(step, oldState, newTr, emptyAttrs);
1540
+ log.info('CHANGES: ', steps);
1541
+ // deleted and merged really...
1542
+ const deleted = steps.filter((s) => s.type !== 'insert-slice');
1543
+ const inserted = steps.filter((s) => s.type === 'insert-slice');
1544
+ steps = diffChangeSteps(deleted, inserted, newTr, oldState.schema);
1545
+ log.info('DIFFED STEPS: ', steps);
1546
+ const [mapping, selectionPos] = processChangeSteps(steps, startPos, newTr, emptyAttrs, oldState.schema);
1267
1547
  if (!wasNodeSelection) {
1268
1548
  const sel = getSelectionStaticConstructor(tr.selection);
1269
1549
  // Use Selection.near to fix selections that point to a block node instead of inline content
@@ -1273,11 +1553,17 @@ function trackTransaction(tr, oldState, newTr, userID) {
1273
1553
  newTr.setSelection(near);
1274
1554
  }
1275
1555
  }
1276
- else if (step instanceof prosemirrorTransform.ReplaceAroundStep) {
1277
- trackReplaceAroundStep(step, oldState, newTr, emptyAttrs);
1278
- // } else if (step instanceof AddMarkStep) {
1279
- // } else if (step instanceof RemoveMarkStep) {
1556
+ else if (step instanceof ReplaceAroundStep) {
1557
+ let steps = trackReplaceAroundStep(step, oldState, newTr, emptyAttrs);
1558
+ const deleted = steps.filter((s) => s.type !== 'insert-slice');
1559
+ const inserted = steps.filter((s) => s.type === 'insert-slice');
1560
+ log.info('INSERT STEPS: ', inserted);
1561
+ steps = diffChangeSteps(deleted, inserted, newTr, oldState.schema);
1562
+ log.info('DIFFED STEPS: ', steps);
1563
+ processChangeSteps(steps, tr.selection.from, newTr, emptyAttrs, oldState.schema);
1280
1564
  }
1565
+ // } else if (step instanceof AddMarkStep) {
1566
+ // } else if (step instanceof RemoveMarkStep) {
1281
1567
  // TODO: here we could check whether adjacent inserts & deletes cancel each other out.
1282
1568
  // However, this should not be done by diffing and only matching node or char by char instead since
1283
1569
  // it's A easier and B more intuitive to user.
@@ -1289,24 +1575,24 @@ function trackTransaction(tr, oldState, newTr, userID) {
1289
1575
  tr.getMeta('uiEvent') && newTr.setMeta('uiEvent', tr.getMeta('uiEvent'));
1290
1576
  });
1291
1577
  // This is kinda hacky solution at the moment to maintain NodeSelections over transactions
1292
- // These are required by at least cross-references that need it to activate the selector pop-up
1578
+ // These are required by at least cross-references and links to activate their selector pop-ups
1293
1579
  if (wasNodeSelection) {
1294
- const mappedPos = newTr.mapping.map(tr.selection.from);
1295
- const resPos = newTr.doc.resolve(mappedPos);
1296
- const nodePos = mappedPos - (((_a = resPos.nodeBefore) === null || _a === void 0 ? void 0 : _a.nodeSize) || 0);
1580
+ // And -1 here is necessary to keep the selection pointing at the start of the node
1581
+ // (or something, breaks with cross-references otherwise)
1582
+ const mappedPos = newTr.mapping.map(tr.selection.from, -1);
1297
1583
  const sel = getSelectionStaticConstructor(tr.selection);
1298
- newTr.setSelection(sel.create(newTr.doc, nodePos));
1584
+ newTr.setSelection(sel.create(newTr.doc, mappedPos));
1299
1585
  }
1300
1586
  log.info('NEW transaction', newTr);
1301
1587
  return newTr;
1302
1588
  }
1303
1589
 
1304
- exports.TrackChangesStatus = void 0;
1590
+ var TrackChangesStatus;
1305
1591
  (function (TrackChangesStatus) {
1306
1592
  TrackChangesStatus["enabled"] = "enabled";
1307
1593
  TrackChangesStatus["viewSnapshots"] = "view-snapshots";
1308
1594
  TrackChangesStatus["disabled"] = "disabled";
1309
- })(exports.TrackChangesStatus || (exports.TrackChangesStatus = {}));
1595
+ })(TrackChangesStatus || (TrackChangesStatus = {}));
1310
1596
 
1311
1597
  /*!
1312
1598
  * © 2021 Atypon Systems LLC
@@ -1323,12 +1609,7 @@ exports.TrackChangesStatus = void 0;
1323
1609
  * See the License for the specific language governing permissions and
1324
1610
  * limitations under the License.
1325
1611
  */
1326
- const trackChangesPluginKey = new prosemirrorState.PluginKey('track-changes');
1327
- // TODO remove
1328
- const infiniteLoopCounter = {
1329
- start: 0,
1330
- iters: 0,
1331
- };
1612
+ const trackChangesPluginKey = new PluginKey('track-changes');
1332
1613
  /**
1333
1614
  * The ProseMirror plugin needed to enable track-changes.
1334
1615
  *
@@ -1341,25 +1622,25 @@ const trackChangesPlugin = (opts = { userID: 'anonymous:Anonymous' }) => {
1341
1622
  if (debug) {
1342
1623
  enableDebug(true);
1343
1624
  }
1344
- return new prosemirrorState.Plugin({
1625
+ return new Plugin({
1345
1626
  key: trackChangesPluginKey,
1346
1627
  props: {
1347
1628
  editable(state) {
1348
1629
  var _a;
1349
- return ((_a = trackChangesPluginKey.getState(state)) === null || _a === void 0 ? void 0 : _a.status) !== exports.TrackChangesStatus.viewSnapshots;
1630
+ return ((_a = trackChangesPluginKey.getState(state)) === null || _a === void 0 ? void 0 : _a.status) !== TrackChangesStatus.viewSnapshots;
1350
1631
  },
1351
1632
  },
1352
1633
  state: {
1353
1634
  init(_config, state) {
1354
1635
  return {
1355
- status: exports.TrackChangesStatus.enabled,
1636
+ status: TrackChangesStatus.enabled,
1356
1637
  userID,
1357
1638
  changeSet: findChanges(state),
1358
1639
  };
1359
1640
  },
1360
1641
  apply(tr, pluginState, _oldState, newState) {
1361
- const setUserID = getAction(tr, exports.TrackChangesAction.setUserID);
1362
- const setStatus = getAction(tr, exports.TrackChangesAction.setPluginStatus);
1642
+ const setUserID = getAction(tr, TrackChangesAction.setUserID);
1643
+ const setStatus = getAction(tr, TrackChangesAction.setPluginStatus);
1363
1644
  if (setUserID) {
1364
1645
  return { ...pluginState, userID: setUserID };
1365
1646
  }
@@ -1370,12 +1651,12 @@ const trackChangesPlugin = (opts = { userID: 'anonymous:Anonymous' }) => {
1370
1651
  changeSet: findChanges(newState),
1371
1652
  };
1372
1653
  }
1373
- else if (pluginState.status === exports.TrackChangesStatus.disabled) {
1654
+ else if (pluginState.status === TrackChangesStatus.disabled) {
1374
1655
  return { ...pluginState, changeSet: new ChangeSet() };
1375
1656
  }
1376
1657
  let { changeSet, ...rest } = pluginState;
1377
- const updatedChangeIds = getAction(tr, exports.TrackChangesAction.updateChanges);
1378
- if (updatedChangeIds || getAction(tr, exports.TrackChangesAction.refreshChanges)) {
1658
+ const updatedChangeIds = getAction(tr, TrackChangesAction.updateChanges);
1659
+ if (updatedChangeIds || getAction(tr, TrackChangesAction.refreshChanges)) {
1379
1660
  changeSet = findChanges(newState);
1380
1661
  }
1381
1662
  return {
@@ -1394,46 +1675,37 @@ const trackChangesPlugin = (opts = { userID: 'anonymous:Anonymous' }) => {
1394
1675
  appendTransaction(trs, oldState, newState) {
1395
1676
  const pluginState = trackChangesPluginKey.getState(newState);
1396
1677
  if (!pluginState ||
1397
- pluginState.status === exports.TrackChangesStatus.disabled ||
1678
+ pluginState.status === TrackChangesStatus.disabled ||
1398
1679
  !(editorView === null || editorView === void 0 ? void 0 : editorView.editable)) {
1399
1680
  return null;
1400
1681
  }
1401
- if (infiniteLoopCounter.start < Date.now() - 10000) {
1402
- infiniteLoopCounter.start = Date.now();
1403
- infiniteLoopCounter.iters = 0;
1404
- }
1405
- if (infiniteLoopCounter.iters >= 100) {
1406
- console.error('Detected probable infinite loop in track changes!');
1407
- return null;
1408
- }
1409
1682
  const { userID, changeSet } = pluginState;
1410
1683
  let createdTr = newState.tr, docChanged = false;
1411
1684
  log.info('TRS', trs);
1412
1685
  trs.forEach((tr) => {
1413
1686
  const wasAppended = tr.getMeta('appendedTransaction');
1414
1687
  const skipMetaUsed = skipTrsWithMetas.some((m) => tr.getMeta(m) || (wasAppended === null || wasAppended === void 0 ? void 0 : wasAppended.getMeta(m)));
1415
- const skipTrackUsed = getAction(tr, exports.TrackChangesAction.skipTrack) ||
1416
- (wasAppended && getAction(wasAppended, exports.TrackChangesAction.skipTrack));
1688
+ const skipTrackUsed = getAction(tr, TrackChangesAction.skipTrack) ||
1689
+ (wasAppended && getAction(wasAppended, TrackChangesAction.skipTrack));
1417
1690
  if (tr.docChanged && !skipMetaUsed && !skipTrackUsed && !tr.getMeta('history$')) {
1418
1691
  createdTr = trackTransaction(tr, oldState, createdTr, userID);
1419
- infiniteLoopCounter.iters += 1;
1420
1692
  }
1421
1693
  docChanged = docChanged || tr.docChanged;
1422
- const setChangeStatuses = getAction(tr, exports.TrackChangesAction.setChangeStatuses);
1694
+ const setChangeStatuses = getAction(tr, TrackChangesAction.setChangeStatuses);
1423
1695
  if (setChangeStatuses) {
1424
1696
  const { status, ids } = setChangeStatuses;
1425
1697
  ids.forEach((changeId) => {
1426
1698
  const change = changeSet === null || changeSet === void 0 ? void 0 : changeSet.get(changeId);
1427
1699
  if (change) {
1428
- createdTr = updateChangeAttrs(createdTr, change, { status }, oldState.schema);
1429
- setAction(createdTr, exports.TrackChangesAction.updateChanges, [change.id]);
1700
+ createdTr = updateChangeAttrs(createdTr, change, { status, reviewedByID: userID }, oldState.schema);
1701
+ setAction(createdTr, TrackChangesAction.updateChanges, [change.id]);
1430
1702
  }
1431
1703
  });
1432
1704
  }
1433
- else if (getAction(tr, exports.TrackChangesAction.applyAndRemoveChanges)) {
1705
+ else if (getAction(tr, TrackChangesAction.applyAndRemoveChanges)) {
1434
1706
  const mapping = applyAcceptedRejectedChanges(createdTr, oldState.schema, changeSet.nodeChanges);
1435
1707
  applyAcceptedRejectedChanges(createdTr, oldState.schema, changeSet.textChanges, mapping);
1436
- setAction(createdTr, exports.TrackChangesAction.refreshChanges, true);
1708
+ setAction(createdTr, TrackChangesAction.refreshChanges, true);
1437
1709
  }
1438
1710
  });
1439
1711
  const changed = pluginState.changeSet.hasInconsistentData &&
@@ -1443,28 +1715,13 @@ const trackChangesPlugin = (opts = { userID: 'anonymous:Anonymous' }) => {
1443
1715
  }
1444
1716
  if (docChanged || createdTr.docChanged || changed) {
1445
1717
  createdTr.setMeta('origin', trackChangesPluginKey);
1446
- return setAction(createdTr, exports.TrackChangesAction.refreshChanges, true);
1718
+ return setAction(createdTr, TrackChangesAction.refreshChanges, true);
1447
1719
  }
1448
1720
  return null;
1449
1721
  },
1450
1722
  });
1451
1723
  };
1452
1724
 
1453
- /*!
1454
- * © 2021 Atypon Systems LLC
1455
- *
1456
- * Licensed under the Apache License, Version 2.0 (the "License");
1457
- * you may not use this file except in compliance with the License.
1458
- * You may obtain a copy of the License at
1459
- *
1460
- * http://www.apache.org/licenses/LICENSE-2.0
1461
- *
1462
- * Unless required by applicable law or agreed to in writing, software
1463
- * distributed under the License is distributed on an "AS IS" BASIS,
1464
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1465
- * See the License for the specific language governing permissions and
1466
- * limitations under the License.
1467
- */
1468
1725
  /**
1469
1726
  * Sets track-changes plugin's status to any of: 'enabled' 'disabled' 'viewSnapshots'. Passing undefined will
1470
1727
  * set 'enabled' status to 'disabled' and 'disabled' | 'viewSnapshots' status to 'enabled'.
@@ -1481,11 +1738,11 @@ const setTrackingStatus = (status) => (state, dispatch) => {
1481
1738
  let newStatus = status;
1482
1739
  if (newStatus === undefined) {
1483
1740
  newStatus =
1484
- currentStatus === exports.TrackChangesStatus.enabled
1485
- ? exports.TrackChangesStatus.disabled
1486
- : exports.TrackChangesStatus.enabled;
1741
+ currentStatus === TrackChangesStatus.enabled
1742
+ ? TrackChangesStatus.disabled
1743
+ : TrackChangesStatus.enabled;
1487
1744
  }
1488
- dispatch && dispatch(setAction(state.tr, exports.TrackChangesAction.setPluginStatus, newStatus));
1745
+ dispatch && dispatch(setAction(state.tr, TrackChangesAction.setPluginStatus, newStatus));
1489
1746
  return true;
1490
1747
  }
1491
1748
  return false;
@@ -1497,7 +1754,7 @@ const setTrackingStatus = (status) => (state, dispatch) => {
1497
1754
  */
1498
1755
  const setChangeStatuses = (status, ids) => (state, dispatch) => {
1499
1756
  dispatch &&
1500
- dispatch(setAction(state.tr, exports.TrackChangesAction.setChangeStatuses, {
1757
+ dispatch(setAction(state.tr, TrackChangesAction.setChangeStatuses, {
1501
1758
  status,
1502
1759
  ids,
1503
1760
  }));
@@ -1508,21 +1765,21 @@ const setChangeStatuses = (status, ids) => (state, dispatch) => {
1508
1765
  * @param userID
1509
1766
  */
1510
1767
  const setUserID = (userID) => (state, dispatch) => {
1511
- dispatch && dispatch(setAction(state.tr, exports.TrackChangesAction.setUserID, userID));
1768
+ dispatch && dispatch(setAction(state.tr, TrackChangesAction.setUserID, userID));
1512
1769
  return true;
1513
1770
  };
1514
1771
  /**
1515
1772
  * Appends a transaction that applies all 'accepted' and 'rejected' changes to the document.
1516
1773
  */
1517
1774
  const applyAndRemoveChanges = () => (state, dispatch) => {
1518
- dispatch && dispatch(setAction(state.tr, exports.TrackChangesAction.applyAndRemoveChanges, true));
1775
+ dispatch && dispatch(setAction(state.tr, TrackChangesAction.applyAndRemoveChanges, true));
1519
1776
  return true;
1520
1777
  };
1521
1778
  /**
1522
1779
  * Runs `findChanges` to iterate over the document to collect changes into a new ChangeSet.
1523
1780
  */
1524
1781
  const refreshChanges = () => (state, dispatch) => {
1525
- dispatch && dispatch(setAction(state.tr, exports.TrackChangesAction.updateChanges, []));
1782
+ dispatch && dispatch(setAction(state.tr, TrackChangesAction.updateChanges, []));
1526
1783
  return true;
1527
1784
  };
1528
1785
  /**
@@ -1552,10 +1809,4 @@ var commands = /*#__PURE__*/Object.freeze({
1552
1809
  setParagraphTestAttribute: setParagraphTestAttribute
1553
1810
  });
1554
1811
 
1555
- exports.ChangeSet = ChangeSet;
1556
- exports.enableDebug = enableDebug;
1557
- exports.getAction = getAction;
1558
- exports.setAction = setAction;
1559
- exports.trackChangesPlugin = trackChangesPlugin;
1560
- exports.trackChangesPluginKey = trackChangesPluginKey;
1561
- exports.trackCommands = commands;
1812
+ export { CHANGE_OPERATION, CHANGE_STATUS, ChangeSet, TrackChangesStatus, enableDebug, skipTracking, trackChangesPlugin, trackChangesPluginKey, commands as trackCommands };