@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
@@ -1,7 +1,15 @@
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';
1
+ 'use strict';
2
+
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);
5
13
 
6
14
  var TrackChangesAction;
7
15
  (function (TrackChangesAction) {
@@ -30,7 +38,14 @@ function getAction(tr, action) {
30
38
  */
31
39
  function setAction(tr, action, payload) {
32
40
  return tr.setMeta(action, payload);
33
- }
41
+ }
42
+ /**
43
+ * Skip tracking for a transaction, use this with caution to avoid race-conditions or just to otherwise
44
+ * omitting applying of track attributes or marks.
45
+ * @param tr
46
+ * @returns
47
+ */
48
+ const skipTracking = (tr) => setAction(tr, TrackChangesAction.skipTrack, true);
34
49
 
35
50
  /******************************************************************************
36
51
  Copyright (c) Microsoft Corporation.
@@ -75,22 +90,22 @@ function __classPrivateFieldSet(receiver, state, value, kind, f) {
75
90
  * See the License for the specific language governing permissions and
76
91
  * limitations under the License.
77
92
  */
78
- var CHANGE_OPERATION;
93
+ exports.CHANGE_OPERATION = void 0;
79
94
  (function (CHANGE_OPERATION) {
80
95
  CHANGE_OPERATION["insert"] = "insert";
81
96
  CHANGE_OPERATION["delete"] = "delete";
82
- CHANGE_OPERATION["set_node_attributes"] = "set_node_attributes";
83
- CHANGE_OPERATION["wrap_with_node"] = "wrap_with_node";
84
- CHANGE_OPERATION["unwrap_from_node"] = "unwrap_from_node";
85
- CHANGE_OPERATION["add_mark"] = "add_mark";
86
- CHANGE_OPERATION["remove_mark"] = "remove_mark";
87
- })(CHANGE_OPERATION || (CHANGE_OPERATION = {}));
88
- var CHANGE_STATUS;
97
+ CHANGE_OPERATION["set_node_attributes"] = "set_attrs";
98
+ // wrap_with_node = 'wrap_with_node',
99
+ // unwrap_from_node = 'unwrap_from_node',
100
+ // add_mark = 'add_mark',
101
+ // remove_mark = 'remove_mark',
102
+ })(exports.CHANGE_OPERATION || (exports.CHANGE_OPERATION = {}));
103
+ exports.CHANGE_STATUS = void 0;
89
104
  (function (CHANGE_STATUS) {
90
105
  CHANGE_STATUS["accepted"] = "accepted";
91
106
  CHANGE_STATUS["rejected"] = "rejected";
92
107
  CHANGE_STATUS["pending"] = "pending";
93
- })(CHANGE_STATUS || (CHANGE_STATUS = {}));
108
+ })(exports.CHANGE_STATUS || (exports.CHANGE_STATUS = {}));
94
109
 
95
110
  /*!
96
111
  * © 2021 Atypon Systems LLC
@@ -107,7 +122,7 @@ var CHANGE_STATUS;
107
122
  * See the License for the specific language governing permissions and
108
123
  * limitations under the License.
109
124
  */
110
- const logger = debug('track');
125
+ const logger = debug__default["default"]('track');
111
126
  const log = {
112
127
  info(str, obj) {
113
128
  if (obj) {
@@ -140,10 +155,10 @@ const log = {
140
155
  */
141
156
  const enableDebug = (enabled) => {
142
157
  if (enabled) {
143
- debug.enable('track');
158
+ debug__default["default"].enable('track');
144
159
  }
145
160
  else {
146
- debug.disable();
161
+ debug__default["default"].disable();
147
162
  }
148
163
  };
149
164
 
@@ -184,16 +199,13 @@ class ChangeSet {
184
199
  rootNodes.push(currentNodeChange);
185
200
  currentNodeChange = undefined;
186
201
  }
187
- if (c.type === 'node-change' && currentNodeChange && c.from < currentNodeChange.to) {
202
+ if (currentNodeChange && c.from < currentNodeChange.to) {
188
203
  currentNodeChange.children.push(c);
189
204
  }
190
205
  else if (c.type === 'node-change') {
191
206
  currentNodeChange = { ...c, children: [] };
192
207
  }
193
- else if (c.type === 'text-change' && currentNodeChange && c.from < currentNodeChange.to) {
194
- currentNodeChange.children.push(c);
195
- }
196
- else if (c.type === 'text-change') {
208
+ else {
197
209
  rootNodes.push(c);
198
210
  }
199
211
  });
@@ -203,13 +215,13 @@ class ChangeSet {
203
215
  return rootNodes;
204
216
  }
205
217
  get pending() {
206
- return this.changeTree.filter((c) => c.attrs.status === CHANGE_STATUS.pending);
218
+ return this.changeTree.filter((c) => c.attrs.status === exports.CHANGE_STATUS.pending);
207
219
  }
208
220
  get accepted() {
209
- return this.changeTree.filter((c) => c.attrs.status === CHANGE_STATUS.accepted);
221
+ return this.changeTree.filter((c) => c.attrs.status === exports.CHANGE_STATUS.accepted);
210
222
  }
211
223
  get rejected() {
212
- return this.changeTree.filter((c) => c.attrs.status === CHANGE_STATUS.rejected);
224
+ return this.changeTree.filter((c) => c.attrs.status === exports.CHANGE_STATUS.rejected);
213
225
  }
214
226
  get textChanges() {
215
227
  return this.changes.filter((c) => c.type === 'text-change');
@@ -217,6 +229,9 @@ class ChangeSet {
217
229
  get nodeChanges() {
218
230
  return this.changes.filter((c) => c.type === 'node-change');
219
231
  }
232
+ get nodeAttrChanges() {
233
+ return this.changes.filter((c) => c.type === 'node-attr-change');
234
+ }
220
235
  get isEmpty() {
221
236
  return __classPrivateFieldGet(this, _ChangeSet_changes, "f").length === 0;
222
237
  }
@@ -262,9 +277,7 @@ class ChangeSet {
262
277
  * @param change
263
278
  */
264
279
  static shouldNotDelete(change) {
265
- const { status, operation } = change.attrs;
266
- return ((operation === CHANGE_OPERATION.insert && status === CHANGE_STATUS.accepted) ||
267
- (operation === CHANGE_OPERATION.delete && status === CHANGE_STATUS.rejected));
280
+ return !ChangeSet.shouldDeleteChange(change);
268
281
  }
269
282
  /**
270
283
  * Determines whether a change should be deleted when applying it to the document.
@@ -272,8 +285,8 @@ class ChangeSet {
272
285
  */
273
286
  static shouldDeleteChange(change) {
274
287
  const { status, operation } = change.attrs;
275
- return ((operation === CHANGE_OPERATION.insert && status === CHANGE_STATUS.rejected) ||
276
- (operation === CHANGE_OPERATION.delete && status === CHANGE_STATUS.accepted));
288
+ return ((operation === exports.CHANGE_OPERATION.insert && status === exports.CHANGE_STATUS.rejected) ||
289
+ (operation === exports.CHANGE_OPERATION.delete && status === exports.CHANGE_STATUS.accepted));
277
290
  }
278
291
  /**
279
292
  * Checks whether change attributes contain all TrackedAttrs keys with non-undefined values
@@ -283,10 +296,22 @@ class ChangeSet {
283
296
  if ('attrs' in attrs) {
284
297
  log.warn('passed "attrs" as property to isValidTrackedAttrs(attrs)', attrs);
285
298
  }
286
- const trackedKeys = ['id', 'userID', 'operation', 'status', 'createdAt'];
287
- const entries = Object.entries(attrs);
299
+ const trackedKeys = [
300
+ 'id',
301
+ 'authorID',
302
+ 'operation',
303
+ 'status',
304
+ 'createdAt',
305
+ 'updatedAt',
306
+ ];
307
+ // reviewedByID is set optional since either ProseMirror or Yjs doesn't like persisting null values inside attributes objects
308
+ // So it can be either omitted completely or at least be null or string
309
+ const optionalKeys = ['reviewedByID'];
310
+ const entries = Object.entries(attrs).filter(([key, val]) => trackedKeys.includes(key));
311
+ const optionalEntries = Object.entries(attrs).filter(([key, val]) => optionalKeys.includes(key));
288
312
  return (entries.length === trackedKeys.length &&
289
313
  entries.every(([key, val]) => trackedKeys.includes(key) && val !== undefined) &&
314
+ optionalEntries.every(([key, val]) => optionalKeys.includes(key) && val !== undefined) &&
290
315
  (attrs.id || '').length > 0 // Changes created with undefined id have '' as placeholder
291
316
  );
292
317
  }
@@ -296,9 +321,89 @@ class ChangeSet {
296
321
  static isNodeChange(change) {
297
322
  return change.type === 'node-change';
298
323
  }
324
+ static isNodeAttrChange(change) {
325
+ return change.type === 'node-attr-change';
326
+ }
299
327
  }
300
328
  _ChangeSet_changes = new WeakMap();
301
329
 
330
+ /*!
331
+ * © 2021 Atypon Systems LLC
332
+ *
333
+ * Licensed under the Apache License, Version 2.0 (the "License");
334
+ * you may not use this file except in compliance with the License.
335
+ * You may obtain a copy of the License at
336
+ *
337
+ * http://www.apache.org/licenses/LICENSE-2.0
338
+ *
339
+ * Unless required by applicable law or agreed to in writing, software
340
+ * distributed under the License is distributed on an "AS IS" BASIS,
341
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
342
+ * See the License for the specific language governing permissions and
343
+ * limitations under the License.
344
+ */
345
+ function uuidv4() {
346
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
347
+ const r = (Math.random() * 16) | 0, v = c == 'x' ? r : (r & 0x3) | 0x8;
348
+ return v.toString(16);
349
+ });
350
+ }
351
+
352
+ function addTrackIdIfDoesntExist(attrs) {
353
+ if (!attrs.id) {
354
+ return {
355
+ id: uuidv4(),
356
+ ...attrs,
357
+ };
358
+ }
359
+ return attrs;
360
+ }
361
+ function getInlineNodeTrackedMarkData(node, schema) {
362
+ if (!node || !node.isInline) {
363
+ return undefined;
364
+ }
365
+ const marksTrackedData = [];
366
+ node.marks.forEach((mark) => {
367
+ if (mark.type === schema.marks.tracked_insert || mark.type === schema.marks.tracked_delete) {
368
+ const operation = mark.type === schema.marks.tracked_insert
369
+ ? exports.CHANGE_OPERATION.insert
370
+ : exports.CHANGE_OPERATION.delete;
371
+ marksTrackedData.push({ ...mark.attrs.dataTracked, operation });
372
+ }
373
+ });
374
+ if (marksTrackedData.length > 1) {
375
+ log.warn('inline node with more than 1 of tracked marks', marksTrackedData);
376
+ }
377
+ return marksTrackedData[0] || undefined;
378
+ }
379
+ function getNodeTrackedData(node, schema) {
380
+ return !node
381
+ ? undefined
382
+ : node.isText
383
+ ? getInlineNodeTrackedMarkData(node, schema)
384
+ : node.attrs.dataTracked;
385
+ }
386
+ function equalMarks(n1, n2) {
387
+ return (n1.marks.length === n2.marks.length &&
388
+ n1.marks.every((mark) => n1.marks.find((m) => m.type === mark.type)));
389
+ }
390
+ function shouldMergeTrackedAttributes(left, right) {
391
+ if (!left || !right) {
392
+ log.warn('passed undefined dataTracked attributes to shouldMergeTrackedAttributes', {
393
+ left,
394
+ right,
395
+ });
396
+ return false;
397
+ }
398
+ return (left.status === right.status &&
399
+ left.operation === right.operation &&
400
+ left.authorID === right.authorID);
401
+ }
402
+ function getMergeableMarkTrackedAttrs(node, attrs, schema) {
403
+ const nodeAttrs = getInlineNodeTrackedMarkData(node, schema);
404
+ return nodeAttrs && shouldMergeTrackedAttributes(nodeAttrs, attrs) ? nodeAttrs : null;
405
+ }
406
+
302
407
  /*!
303
408
  * © 2021 Atypon Systems LLC
304
409
  *
@@ -327,15 +432,17 @@ function deleteNode(node, pos, tr) {
327
432
  var _a;
328
433
  const startPos = tr.doc.resolve(pos + 1);
329
434
  const range = startPos.blockRange(tr.doc.resolve(startPos.pos - 2 + node.nodeSize));
330
- const targetDepth = range && liftTarget(range);
331
- // Check with typeof since with old prosemirror-transform targetDepth is undefined
435
+ const targetDepth = range && prosemirrorTransform.liftTarget(range);
436
+ // Check with typeof since with prosemirror-transform pre 1.6.0 targetDepth is undefined
332
437
  if (range && typeof targetDepth === 'number') {
333
438
  return tr.lift(range, targetDepth);
334
439
  }
335
440
  const resPos = tr.doc.resolve(pos);
336
- const canMergeToNodeAbove = (resPos.parent !== tr.doc || resPos.nodeBefore) && ((_a = node.firstChild) === null || _a === void 0 ? void 0 : _a.isText);
441
+ // Block nodes can be deleted by just removing their start token which should then merge the text
442
+ // content to above node's content (if there is one)
443
+ const canMergeToNodeAbove = (resPos.parent !== tr.doc || resPos.nodeBefore) && node.isBlock && ((_a = node.firstChild) === null || _a === void 0 ? void 0 : _a.isText);
337
444
  if (canMergeToNodeAbove) {
338
- return tr.replaceWith(pos - 1, pos + 1, Fragment.empty);
445
+ return tr.replaceWith(pos - 1, pos + 1, prosemirrorModel.Fragment.empty);
339
446
  }
340
447
  else {
341
448
  // NOTE: there's an edge case where moving content is not possible but because the immediate
@@ -368,98 +475,22 @@ function deleteNode(node, pos, tr) {
368
475
  */
369
476
  function mergeNode(node, pos, tr) {
370
477
  var _a;
371
- if (canJoin(tr.doc, pos)) {
478
+ if (prosemirrorTransform.canJoin(tr.doc, pos)) {
372
479
  return tr.join(pos);
373
480
  }
374
- else if (canJoin(tr.doc, pos + node.nodeSize)) {
481
+ else if (prosemirrorTransform.canJoin(tr.doc, pos + node.nodeSize)) {
482
+ // TODO should copy the attributes from the merged node below
375
483
  return tr.join(pos + node.nodeSize);
376
484
  }
377
- // TODO is this the same thing as join?
485
+ // TODO is this the same thing as join to above?
378
486
  const resPos = tr.doc.resolve(pos);
379
487
  const canMergeToNodeAbove = (resPos.parent !== tr.doc || resPos.nodeBefore) && ((_a = node.firstChild) === null || _a === void 0 ? void 0 : _a.isText);
380
488
  if (canMergeToNodeAbove) {
381
- return tr.replaceWith(pos - 1, pos + 1, Fragment.empty);
489
+ return tr.replaceWith(pos - 1, pos + 1, prosemirrorModel.Fragment.empty);
382
490
  }
383
491
  return undefined;
384
492
  }
385
493
 
386
- /*!
387
- * © 2021 Atypon Systems LLC
388
- *
389
- * Licensed under the Apache License, Version 2.0 (the "License");
390
- * you may not use this file except in compliance with the License.
391
- * You may obtain a copy of the License at
392
- *
393
- * http://www.apache.org/licenses/LICENSE-2.0
394
- *
395
- * Unless required by applicable law or agreed to in writing, software
396
- * distributed under the License is distributed on an "AS IS" BASIS,
397
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
398
- * See the License for the specific language governing permissions and
399
- * limitations under the License.
400
- */
401
- function uuidv4() {
402
- return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
403
- const r = (Math.random() * 16) | 0, v = c == 'x' ? r : (r & 0x3) | 0x8;
404
- return v.toString(16);
405
- });
406
- }
407
-
408
- function addTrackIdIfDoesntExist(attrs) {
409
- if (!attrs.id) {
410
- return {
411
- id: uuidv4(),
412
- ...attrs,
413
- };
414
- }
415
- return attrs;
416
- }
417
- function getInlineNodeTrackedMarkData(node, schema) {
418
- if (!node || !node.isInline) {
419
- return undefined;
420
- }
421
- const marksTrackedData = [];
422
- node.marks.forEach((mark) => {
423
- if (mark.type === schema.marks.tracked_insert || mark.type === schema.marks.tracked_delete) {
424
- const operation = mark.type === schema.marks.tracked_insert
425
- ? CHANGE_OPERATION.insert
426
- : CHANGE_OPERATION.delete;
427
- marksTrackedData.push({ ...mark.attrs.dataTracked, operation });
428
- }
429
- });
430
- if (marksTrackedData.length > 1) {
431
- log.warn('inline node with more than 1 of tracked marks', marksTrackedData);
432
- }
433
- return marksTrackedData[0] || undefined;
434
- }
435
- function getNodeTrackedData(node, schema) {
436
- return !node
437
- ? undefined
438
- : node.isText
439
- ? getInlineNodeTrackedMarkData(node, schema)
440
- : node.attrs.dataTracked;
441
- }
442
- function equalMarks(n1, n2) {
443
- return (n1.marks.length === n2.marks.length &&
444
- n1.marks.every((mark) => n1.marks.find((m) => m.type === mark.type)));
445
- }
446
- function shouldMergeTrackedAttributes(left, right) {
447
- if (!left || !right) {
448
- log.warn('passed undefined dataTracked attributes to shouldMergeTrackedAttributes', {
449
- left,
450
- right,
451
- });
452
- return false;
453
- }
454
- return (left.status === right.status &&
455
- left.operation === right.operation &&
456
- left.userID === right.userID);
457
- }
458
- function getMergeableMarkTrackedAttrs(node, attrs, schema) {
459
- const nodeAttrs = getInlineNodeTrackedMarkData(node, schema);
460
- return nodeAttrs && shouldMergeTrackedAttributes(nodeAttrs, attrs) ? nodeAttrs : null;
461
- }
462
-
463
494
  function updateChangeAttrs(tr, change, trackedAttrs, schema) {
464
495
  const node = tr.doc.nodeAt(change.from);
465
496
  if (!node) {
@@ -497,9 +528,9 @@ function updateChangeChildrenAttributes(changes, tr, mapping) {
497
528
  * @param changes
498
529
  * @param deleteMap
499
530
  */
500
- function applyAcceptedRejectedChanges(tr, schema, changes, deleteMap = new Mapping()) {
531
+ function applyAcceptedRejectedChanges(tr, schema, changes, deleteMap = new prosemirrorTransform.Mapping()) {
501
532
  changes.forEach((change) => {
502
- if (change.attrs.status === CHANGE_STATUS.pending) {
533
+ if (change.attrs.status === exports.CHANGE_STATUS.pending) {
503
534
  return;
504
535
  }
505
536
  // Map change.from and skip those which dont need to be applied
@@ -532,6 +563,16 @@ function applyAcceptedRejectedChanges(tr, schema, changes, deleteMap = new Mappi
532
563
  }
533
564
  deleteMap.appendMap(tr.steps[tr.steps.length - 1].getMap());
534
565
  }
566
+ else if (ChangeSet.isNodeAttrChange(change) &&
567
+ change.attrs.status === exports.CHANGE_STATUS.accepted) {
568
+ const attrs = { ...node.attrs, dataTracked: null };
569
+ tr.setNodeMarkup(from, undefined, attrs, node.marks);
570
+ }
571
+ else if (ChangeSet.isNodeAttrChange(change) &&
572
+ change.attrs.status === exports.CHANGE_STATUS.rejected) {
573
+ const attrs = { ...change.oldAttrs, dataTracked: null };
574
+ tr.setNodeMarkup(from, undefined, attrs, node.marks);
575
+ }
535
576
  });
536
577
  return deleteMap;
537
578
  }
@@ -573,6 +614,7 @@ function findChanges(state) {
573
614
  type: 'text-change',
574
615
  from: pos,
575
616
  to: pos + node.nodeSize,
617
+ text: node.text,
576
618
  attrs,
577
619
  },
578
620
  node,
@@ -617,13 +659,15 @@ function fixInconsistentChanges(changeSet, trackUserID, newTr, schema) {
617
659
  const iteratedIds = new Set();
618
660
  let changed = false;
619
661
  changeSet.invalidChanges.forEach((c) => {
620
- const { id, userID, operation, status, createdAt } = c.attrs;
662
+ const { id, authorID, operation, reviewedByID, status, createdAt, updatedAt } = c.attrs;
621
663
  const newAttrs = {
622
664
  ...((!id || iteratedIds.has(id) || id.length === 0) && { id: uuidv4() }),
623
- ...(!userID && { userID: trackUserID }),
624
- ...(!operation && { operation: CHANGE_OPERATION.insert }),
625
- ...(!status && { status: CHANGE_STATUS.pending }),
665
+ ...(!authorID && { authorID: trackUserID }),
666
+ ...(!operation && { operation: exports.CHANGE_OPERATION.insert }),
667
+ ...(!reviewedByID && { reviewedByID: null }),
668
+ ...(!status && { status: exports.CHANGE_STATUS.pending }),
626
669
  ...(!createdAt && { createdAt: Date.now() }),
670
+ ...(!updatedAt && { updatedAt: Date.now() }),
627
671
  };
628
672
  if (Object.keys(newAttrs).length > 0) {
629
673
  updateChangeAttrs(newTr, c, { ...c.attrs, ...newAttrs }, schema);
@@ -634,87 +678,6 @@ function fixInconsistentChanges(changeSet, trackUserID, newTr, schema) {
634
678
  return changed;
635
679
  }
636
680
 
637
- /*!
638
- * © 2021 Atypon Systems LLC
639
- *
640
- * Licensed under the Apache License, Version 2.0 (the "License");
641
- * you may not use this file except in compliance with the License.
642
- * You may obtain a copy of the License at
643
- *
644
- * http://www.apache.org/licenses/LICENSE-2.0
645
- *
646
- * Unless required by applicable law or agreed to in writing, software
647
- * distributed under the License is distributed on an "AS IS" BASIS,
648
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
649
- * See the License for the specific language governing permissions and
650
- * limitations under the License.
651
- */
652
- function markInlineNodeChange(node, newTrackAttrs, schema) {
653
- const filtered = node.marks.filter((m) => m.type !== schema.marks.tracked_insert && m.type !== schema.marks.tracked_delete);
654
- const mark = newTrackAttrs.operation === CHANGE_OPERATION.insert
655
- ? schema.marks.tracked_insert
656
- : schema.marks.tracked_delete;
657
- const createdMark = mark.create({
658
- dataTracked: addTrackIdIfDoesntExist(newTrackAttrs),
659
- });
660
- return node.mark(filtered.concat(createdMark));
661
- }
662
- function recurseNodeContent(node, newTrackAttrs, schema) {
663
- if (node.isText) {
664
- return markInlineNodeChange(node, newTrackAttrs, schema);
665
- }
666
- else if (node.isBlock || node.isInline) {
667
- const updatedChildren = [];
668
- node.content.forEach((child) => {
669
- updatedChildren.push(recurseNodeContent(child, newTrackAttrs, schema));
670
- });
671
- return node.type.create({
672
- ...node.attrs,
673
- dataTracked: addTrackIdIfDoesntExist(newTrackAttrs),
674
- }, Fragment.fromArray(updatedChildren), node.marks);
675
- }
676
- else {
677
- log.error(`unhandled node type: "${node.type.name}"`, node);
678
- return node;
679
- }
680
- }
681
- function setFragmentAsInserted(inserted, insertAttrs, schema) {
682
- // Recurse the content in the inserted slice and either mark it tracked_insert or set node attrs
683
- const updatedInserted = [];
684
- inserted.forEach((n) => {
685
- updatedInserted.push(recurseNodeContent(n, insertAttrs, schema));
686
- });
687
- return updatedInserted.length === 0 ? inserted : Fragment.fromArray(updatedInserted);
688
- }
689
-
690
- /*!
691
- * © 2021 Atypon Systems LLC
692
- *
693
- * Licensed under the Apache License, Version 2.0 (the "License");
694
- * you may not use this file except in compliance with the License.
695
- * You may obtain a copy of the License at
696
- *
697
- * http://www.apache.org/licenses/LICENSE-2.0
698
- *
699
- * Unless required by applicable law or agreed to in writing, software
700
- * distributed under the License is distributed on an "AS IS" BASIS,
701
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
702
- * See the License for the specific language governing permissions and
703
- * limitations under the License.
704
- */
705
- function createNewInsertAttrs(attrs) {
706
- return {
707
- ...attrs,
708
- operation: CHANGE_OPERATION.insert,
709
- };
710
- }
711
- function createNewDeleteAttrs(attrs) {
712
- return {
713
- ...attrs,
714
- operation: CHANGE_OPERATION.delete,
715
- };
716
- }
717
-
718
681
  /*!
719
682
  * © 2021 Atypon Systems LLC
720
683
  *
@@ -754,7 +717,7 @@ function getMergedNode(node, currentDepth, depth, first) {
754
717
  };
755
718
  }
756
719
  const result = [];
757
- let merged = Fragment.empty;
720
+ let merged = prosemirrorModel.Fragment.empty;
758
721
  node.content.forEach((n, _, i) => {
759
722
  if ((first && i === 0) || (!first && i === node.childCount - 1)) {
760
723
  const { mergedNodeContent, unmergedContent } = getMergedNode(n, currentDepth + 1, depth, first);
@@ -769,7 +732,7 @@ function getMergedNode(node, currentDepth, depth, first) {
769
732
  });
770
733
  return {
771
734
  mergedNodeContent: merged,
772
- unmergedContent: result.length > 0 ? Fragment.fromArray(result) : undefined,
735
+ unmergedContent: result.length > 0 ? prosemirrorModel.Fragment.fromArray(result) : undefined,
773
736
  };
774
737
  }
775
738
  /**
@@ -810,70 +773,104 @@ function splitSliceIntoMergedParts(insertSlice, mergeEqualSides = false) {
810
773
  firstMergedNode,
811
774
  lastMergedNode,
812
775
  };
813
- }
814
- /**
815
- * Deletes inserted text directly, otherwise wraps it with tracked_delete mark
776
+ }
777
+
778
+ /*!
779
+ * © 2021 Atypon Systems LLC
816
780
  *
817
- * This would work for general inline nodes too, but since node marks don't work properly
818
- * with Yjs, attributes are used instead.
819
- * @param node
820
- * @param pos
821
- * @param newTr
822
- * @param schema
823
- * @param deleteAttrs
824
- * @param from
825
- * @param to
781
+ * Licensed under the Apache License, Version 2.0 (the "License");
782
+ * you may not use this file except in compliance with the License.
783
+ * You may obtain a copy of the License at
784
+ *
785
+ * http://www.apache.org/licenses/LICENSE-2.0
786
+ *
787
+ * Unless required by applicable law or agreed to in writing, software
788
+ * distributed under the License is distributed on an "AS IS" BASIS,
789
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
790
+ * See the License for the specific language governing permissions and
791
+ * limitations under the License.
826
792
  */
827
- function deleteTextIfInserted(node, pos, newTr, schema, deleteAttrs, from, to) {
828
- const start = from ? Math.max(pos, from) : pos;
829
- const nodeEnd = pos + node.nodeSize;
830
- const end = to ? Math.min(nodeEnd, to) : nodeEnd;
831
- if (node.marks.find((m) => m.type === schema.marks.tracked_insert)) {
832
- // Math.max(pos, from) is for picking always the start of the node,
833
- // not the start of the change (which might span multiple nodes).
834
- // Pos can be less than from as nodesBetween iterates through all nodes starting from the top block node
835
- newTr.replaceWith(start, end, Fragment.empty);
836
- return start;
793
+ function markInlineNodeChange(node, newTrackAttrs, schema) {
794
+ const filtered = node.marks.filter((m) => m.type !== schema.marks.tracked_insert && m.type !== schema.marks.tracked_delete);
795
+ const mark = newTrackAttrs.operation === exports.CHANGE_OPERATION.insert
796
+ ? schema.marks.tracked_insert
797
+ : schema.marks.tracked_delete;
798
+ const createdMark = mark.create({
799
+ dataTracked: addTrackIdIfDoesntExist(newTrackAttrs),
800
+ });
801
+ return node.mark(filtered.concat(createdMark));
802
+ }
803
+ function recurseNodeContent(node, newTrackAttrs, schema) {
804
+ if (node.isText) {
805
+ return markInlineNodeChange(node, newTrackAttrs, schema);
837
806
  }
838
- else {
839
- const leftNode = newTr.doc.resolve(start).nodeBefore;
840
- const leftMarks = getMergeableMarkTrackedAttrs(leftNode, deleteAttrs, schema);
841
- const rightNode = newTr.doc.resolve(end).nodeAfter;
842
- const rightMarks = getMergeableMarkTrackedAttrs(rightNode, deleteAttrs, schema);
843
- const fromStartOfMark = start - (leftNode && leftMarks ? leftNode.nodeSize : 0);
844
- const toEndOfMark = end + (rightNode && rightMarks ? rightNode.nodeSize : 0);
845
- const dataTracked = addTrackIdIfDoesntExist({
846
- ...leftMarks,
847
- ...rightMarks,
848
- ...deleteAttrs,
807
+ else if (node.isBlock || node.isInline) {
808
+ const updatedChildren = [];
809
+ node.content.forEach((child) => {
810
+ updatedChildren.push(recurseNodeContent(child, newTrackAttrs, schema));
849
811
  });
850
- newTr.addMark(fromStartOfMark, toEndOfMark, schema.marks.tracked_delete.create({
851
- dataTracked,
852
- }));
853
- return toEndOfMark;
854
- }
855
- }
856
- /**
857
- * Deletes inserted block or inline node, otherwise adds `dataTracked` object with CHANGE_STATUS 'deleted'
858
- * @param node
859
- * @param pos
860
- * @param newTr
861
- * @param deleteAttrs
862
- */
863
- function deleteOrSetNodeDeleted(node, pos, newTr, deleteAttrs) {
864
- const dataTracked = node.attrs.dataTracked;
865
- const wasInsertedBySameUser = (dataTracked === null || dataTracked === void 0 ? void 0 : dataTracked.operation) === CHANGE_OPERATION.insert && dataTracked.userID === deleteAttrs.userID;
866
- if (wasInsertedBySameUser) {
867
- deleteNode(node, pos, newTr);
812
+ return node.type.create({
813
+ ...node.attrs,
814
+ dataTracked: addTrackIdIfDoesntExist(newTrackAttrs),
815
+ }, prosemirrorModel.Fragment.fromArray(updatedChildren), node.marks);
868
816
  }
869
817
  else {
870
- const attrs = {
871
- ...node.attrs,
872
- dataTracked: addTrackIdIfDoesntExist(deleteAttrs),
873
- };
874
- newTr.setNodeMarkup(pos, undefined, attrs, node.marks);
818
+ log.error(`unhandled node type: "${node.type.name}"`, node);
819
+ return node;
875
820
  }
876
821
  }
822
+ function setFragmentAsInserted(inserted, insertAttrs, schema) {
823
+ // Recurse the content in the inserted slice and either mark it tracked_insert or set node attrs
824
+ const updatedInserted = [];
825
+ inserted.forEach((n) => {
826
+ updatedInserted.push(recurseNodeContent(n, insertAttrs, schema));
827
+ });
828
+ return updatedInserted.length === 0 ? inserted : prosemirrorModel.Fragment.fromArray(updatedInserted);
829
+ }
830
+
831
+ /*!
832
+ * © 2021 Atypon Systems LLC
833
+ *
834
+ * Licensed under the Apache License, Version 2.0 (the "License");
835
+ * you may not use this file except in compliance with the License.
836
+ * You may obtain a copy of the License at
837
+ *
838
+ * http://www.apache.org/licenses/LICENSE-2.0
839
+ *
840
+ * Unless required by applicable law or agreed to in writing, software
841
+ * distributed under the License is distributed on an "AS IS" BASIS,
842
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
843
+ * See the License for the specific language governing permissions and
844
+ * limitations under the License.
845
+ */
846
+ function createNewInsertAttrs(attrs) {
847
+ return {
848
+ ...attrs,
849
+ operation: exports.CHANGE_OPERATION.insert,
850
+ };
851
+ }
852
+ function createNewDeleteAttrs(attrs) {
853
+ return {
854
+ ...attrs,
855
+ operation: exports.CHANGE_OPERATION.delete,
856
+ };
857
+ }
858
+
859
+ /*!
860
+ * © 2021 Atypon Systems LLC
861
+ *
862
+ * Licensed under the Apache License, Version 2.0 (the "License");
863
+ * you may not use this file except in compliance with the License.
864
+ * You may obtain a copy of the License at
865
+ *
866
+ * http://www.apache.org/licenses/LICENSE-2.0
867
+ *
868
+ * Unless required by applicable law or agreed to in writing, software
869
+ * distributed under the License is distributed on an "AS IS" BASIS,
870
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
871
+ * See the License for the specific language governing permissions and
872
+ * limitations under the License.
873
+ */
877
874
  /**
878
875
  * Applies deletion to the doc without actually deleting nodes that have not been inserted
879
876
  *
@@ -900,28 +897,39 @@ function deleteOrSetNodeDeleted(node, pos, newTr, deleteAttrs) {
900
897
  * @returns mapping adjusted by the applied operations & modified insert slice
901
898
  */
902
899
  function deleteAndMergeSplitNodes(from, to, gap, startDoc, newTr, schema, trackAttrs, insertSlice) {
903
- const deleteMap = new Mapping();
904
- const mergedInsertPos = undefined;
900
+ const deleteMap = new prosemirrorTransform.Mapping();
901
+ const steps = [];
905
902
  // No deletion applied, return default values
906
903
  if (from === to) {
907
904
  return {
908
905
  deleteMap,
909
- mergedInsertPos,
910
906
  newSliceContent: insertSlice.content,
907
+ steps,
911
908
  };
912
909
  }
913
910
  const { openStart, openEnd } = insertSlice;
914
911
  const { updatedSliceNodes, firstMergedNode, lastMergedNode } = splitSliceIntoMergedParts(insertSlice, gap !== undefined);
915
- const deleteAttrs = createNewDeleteAttrs(trackAttrs);
916
912
  let mergingStartSide = true;
917
913
  startDoc.nodesBetween(from, to, (node, pos) => {
918
914
  const { pos: offsetPos, deleted: nodeWasDeleted } = deleteMap.mapResult(pos, 1);
919
915
  const offsetFrom = deleteMap.map(from, -1);
920
916
  const offsetTo = deleteMap.map(to, 1);
921
- const wasWithinGap = gap && offsetPos >= deleteMap.map(gap.start, -1);
922
917
  const nodeEnd = offsetPos + node.nodeSize;
918
+ // So this insane boolean checks for ReplaceAroundStep gaps and whether the node should be skipped
919
+ // since the content inside gap should stay unchanged.
920
+ // All other nodes except text nodes consist of one start and end token (or just a single token for atoms).
921
+ // For them we can just check whether the start token is within the gap eg pos is 10 when gap (8, 18) to
922
+ // determine whether it should be skipped.
923
+ // For text nodes though, since they are continous, they might only partially be enclosed in the gap
924
+ // eg. pos 10 when gap is (8, 18) BUT if their nodeEnd goes past the gap's end eg nodeEnd 20 they actually
925
+ // are altered and should not be skipped.
926
+ // @TODO ATM 20.7.2022 there doesn't seem to be tests that capture this.
927
+ const wasWithinGap = gap &&
928
+ ((!node.isText && offsetPos >= deleteMap.map(gap.start, -1)) ||
929
+ (node.isText &&
930
+ offsetPos <= deleteMap.map(gap.start, -1) &&
931
+ nodeEnd >= deleteMap.map(gap.end, -1)));
923
932
  let step = newTr.steps[newTr.steps.length - 1];
924
- // debugger
925
933
  // nodeEnd > offsetFrom -> delete touches this node
926
934
  // eg (del 6 10) <p 5>|<t 6>cdf</t 9></p 10>| -> <p> nodeEnd 10 > from 6
927
935
  //
@@ -976,24 +984,18 @@ function deleteAndMergeSplitNodes(from, to, gap, startDoc, newTr, schema, trackA
976
984
  // ProseMirror node semantics as start tokens are considered to contain the actual node itself.
977
985
  const mergeEndNode = startTokenDeleted && openEnd > 0 && depth === openEnd && mergeContent !== undefined;
978
986
  if (mergeStartNode || mergeEndNode) {
979
- // The default insert position for block nodes is either the start of the merged content or the end.
980
- // Incase text was merged, this must be updated as the start or end of the node doesn't map to the
981
- // actual position of the merge. Currently the inserted content is inserted at the start or end
982
- // of the merged content, TODO reverse the start/end when end/start token?
983
- let insertPos = mergeStartNode ? nodeEnd - openStart : offsetPos + openEnd;
984
- if (node.isText) {
985
- // When merging text we must delete text in the same go as well, as the from/to boundary goes through
986
- // the text node.
987
- insertPos = deleteTextIfInserted(node, offsetPos, newTr, schema, deleteAttrs, offsetFrom, offsetTo);
988
- deleteMap.appendMap(newTr.steps[newTr.steps.length - 1].getMap());
989
- step = newTr.steps[newTr.steps.length - 1];
990
- }
991
987
  // Just as a fun fact that I found out while debugging this. Inserting text at paragraph position wraps
992
988
  // it into a new paragraph(!). So that's why you always offset your positions to insert it _inside_
993
989
  // the paragraph.
994
- if (mergeContent.size !== 0) {
995
- newTr.insert(insertPos, setFragmentAsInserted(mergeContent, createNewInsertAttrs(trackAttrs), schema));
996
- }
990
+ steps.push({
991
+ type: 'merge-fragment',
992
+ pos: offsetPos,
993
+ mergePos: mergeStartNode ? nodeEnd - openStart : offsetPos + openEnd,
994
+ from: offsetFrom,
995
+ to: offsetTo,
996
+ node,
997
+ fragment: setFragmentAsInserted(mergeContent, createNewInsertAttrs(trackAttrs), schema),
998
+ });
997
999
  // Okay this is a bit ridiculous but it's used to adjust the insert pos when track changes prevents deletions
998
1000
  // of merged nodes & content, as just using mapped toA in that case isn't the same.
999
1001
  // The calculation is a bit mysterious, I admit.
@@ -1006,12 +1008,23 @@ function deleteAndMergeSplitNodes(from, to, gap, startDoc, newTr, schema, trackA
1006
1008
  else if (node.isText) {
1007
1009
  // Text deletion is handled even when the deletion doesn't completely wrap the text node
1008
1010
  // (which is basically the case most of the time)
1009
- deleteTextIfInserted(node, offsetPos, newTr, schema, deleteAttrs, offsetFrom, offsetTo);
1011
+ steps.push({
1012
+ type: 'delete-text',
1013
+ pos: offsetPos,
1014
+ from: Math.max(offsetPos, offsetFrom),
1015
+ to: Math.min(nodeEnd, offsetTo),
1016
+ node,
1017
+ });
1010
1018
  }
1011
1019
  else ;
1012
1020
  }
1013
1021
  else if (nodeCompletelyDeleted) {
1014
- deleteOrSetNodeDeleted(node, offsetPos, newTr, deleteAttrs);
1022
+ steps.push({
1023
+ type: 'delete-node',
1024
+ pos: offsetPos,
1025
+ nodeEnd: nodeEnd,
1026
+ node,
1027
+ });
1015
1028
  }
1016
1029
  }
1017
1030
  const newestStep = newTr.steps[newTr.steps.length - 1];
@@ -1021,10 +1034,10 @@ function deleteAndMergeSplitNodes(from, to, gap, startDoc, newTr, schema, trackA
1021
1034
  });
1022
1035
  return {
1023
1036
  deleteMap,
1024
- mergedInsertPos,
1025
1037
  newSliceContent: updatedSliceNodes
1026
- ? Fragment.fromArray(updatedSliceNodes)
1038
+ ? prosemirrorModel.Fragment.fromArray(updatedSliceNodes)
1027
1039
  : insertSlice.content,
1040
+ steps,
1028
1041
  };
1029
1042
  }
1030
1043
 
@@ -1047,18 +1060,20 @@ function mergeTrackedMarks(pos, doc, newTr, schema) {
1047
1060
  if (!nodeAfter || !nodeBefore || !leftMark || !rightMark || leftMark.type !== rightMark.type) {
1048
1061
  return;
1049
1062
  }
1050
- const leftAttrs = leftMark.attrs;
1051
- const rightAttrs = rightMark.attrs;
1052
- if (!shouldMergeTrackedAttributes(leftAttrs.dataTracked, rightAttrs.dataTracked)) {
1063
+ const leftDataTracked = leftMark.attrs.dataTracked;
1064
+ const rightDataTracked = rightMark.attrs.dataTracked;
1065
+ if (!shouldMergeTrackedAttributes(leftDataTracked, rightDataTracked)) {
1053
1066
  return;
1054
1067
  }
1055
- const newAttrs = {
1056
- ...leftAttrs,
1057
- createdAt: Math.max(leftAttrs.createdAt || 0, rightAttrs.createdAt || 0) || Date.now(),
1068
+ const isLeftOlder = (leftDataTracked.createdAt || 0) < (rightDataTracked.createdAt || 0);
1069
+ const ancestorAttrs = isLeftOlder ? leftDataTracked : rightDataTracked;
1070
+ const dataTracked = {
1071
+ ...ancestorAttrs,
1072
+ updatedAt: Date.now(),
1058
1073
  };
1059
1074
  const fromStartOfMark = pos - nodeBefore.nodeSize;
1060
1075
  const toEndOfMark = pos + nodeAfter.nodeSize;
1061
- newTr.addMark(fromStartOfMark, toEndOfMark, leftMark.type.create(newAttrs));
1076
+ newTr.addMark(fromStartOfMark, toEndOfMark, leftMark.type.create({ ...leftMark.attrs, dataTracked }));
1062
1077
  }
1063
1078
 
1064
1079
  /*!
@@ -1106,14 +1121,16 @@ function trackReplaceAroundStep(step, oldState, newTr, attrs) {
1106
1121
  const stepResult = newTr.maybeStep(newStep);
1107
1122
  if (stepResult.failed) {
1108
1123
  log.error(`inverting ReplaceAroundStep failed: "${stepResult.failed}"`, newStep);
1109
- return;
1124
+ return [];
1110
1125
  }
1111
1126
  const gap = oldState.doc.slice(gapFrom, gapTo);
1112
1127
  log.info('RETAINED GAP CONTENT', gap);
1113
1128
  // First apply the deleted range and update the insert slice to not include content that was deleted,
1114
1129
  // eg partial nodes in an open-ended slice
1115
- const { deleteMap, newSliceContent } = deleteAndMergeSplitNodes(from, to, { start: gapFrom, end: gapTo }, newTr.doc, newTr, oldState.schema, attrs, slice);
1130
+ const { deleteMap, newSliceContent, steps: deleteSteps, } = deleteAndMergeSplitNodes(from, to, { start: gapFrom, end: gapTo }, newTr.doc, newTr, oldState.schema, attrs, slice);
1131
+ let steps = deleteSteps;
1116
1132
  log.info('TR: new steps after applying delete', [...newTr.steps]);
1133
+ log.info('DELETE STEPS: ', deleteSteps);
1117
1134
  // We only want to insert when there something inside the gap (actually would this be always true?)
1118
1135
  // or insert slice wasn't just start/end tokens (which we already merged inside deleteAndMergeSplitBlockNodes)
1119
1136
  if (gap.size > 0 || (!structure && newSliceContent.size > 0)) {
@@ -1122,27 +1139,25 @@ function trackReplaceAroundStep(step, oldState, newTr, attrs) {
1122
1139
  // the sides should be equal. TODO can they be other than 0?
1123
1140
  const openStart = slice.openStart !== slice.openEnd || newSliceContent.size === 0 ? 0 : slice.openStart;
1124
1141
  const openEnd = slice.openStart !== slice.openEnd || newSliceContent.size === 0 ? 0 : slice.openEnd;
1125
- let insertedSlice = new Slice(setFragmentAsInserted(newSliceContent, createNewInsertAttrs(attrs), oldState.schema), openStart, openEnd);
1142
+ let insertedSlice = new prosemirrorModel.Slice(setFragmentAsInserted(newSliceContent, createNewInsertAttrs(attrs), oldState.schema), openStart, openEnd);
1126
1143
  if (gap.size > 0) {
1127
1144
  log.info('insertedSlice before inserted gap', insertedSlice);
1128
1145
  insertedSlice = insertedSlice.insertAt(insertedSlice.size === 0 ? 0 : insert, gap.content);
1129
1146
  log.info('insertedSlice after inserted gap', insertedSlice);
1130
1147
  }
1131
- const newStep = new ReplaceStep(deleteMap.map(gapFrom), deleteMap.map(gapTo), insertedSlice, false);
1132
- const stepResult = newTr.maybeStep(newStep);
1133
- if (stepResult.failed) {
1134
- log.error(`insert ReplaceStep failed: "${stepResult.failed}"`, newStep);
1135
- return;
1136
- }
1137
- log.info('new steps after applying insert', [...newTr.steps]);
1138
- mergeTrackedMarks(deleteMap.map(gapFrom), newTr.doc, newTr, oldState.schema);
1139
- mergeTrackedMarks(deleteMap.map(gapTo), newTr.doc, newTr, oldState.schema);
1148
+ deleteSteps.push({
1149
+ type: 'insert-slice',
1150
+ from: deleteMap.map(gapFrom),
1151
+ to: deleteMap.map(gapTo),
1152
+ slice: insertedSlice,
1153
+ });
1140
1154
  }
1141
1155
  else {
1142
1156
  // Incase only deletion was applied, check whether tracked marks around deleted content can be merged
1143
1157
  mergeTrackedMarks(deleteMap.map(gapFrom), newTr.doc, newTr, oldState.schema);
1144
1158
  mergeTrackedMarks(deleteMap.map(gapTo), newTr.doc, newTr, oldState.schema);
1145
1159
  }
1160
+ return steps;
1146
1161
  }
1147
1162
 
1148
1163
  /*!
@@ -1162,7 +1177,7 @@ function trackReplaceAroundStep(step, oldState, newTr, attrs) {
1162
1177
  */
1163
1178
  function trackReplaceStep(step, oldState, newTr, attrs) {
1164
1179
  log.info('###### ReplaceStep ######');
1165
- let selectionPos = 0;
1180
+ let selectionPos = 0, changeSteps = [];
1166
1181
  step.getMap().forEach((fromA, toA, fromB, toB) => {
1167
1182
  log.info(`changed ranges: ${fromA} ${toA} ${fromB} ${toB}`);
1168
1183
  const { slice } = step;
@@ -1173,36 +1188,309 @@ function trackReplaceStep(step, oldState, newTr, attrs) {
1173
1188
  log.error(`invert ReplaceStep failed: "${stepResult.failed}"`, newStep);
1174
1189
  return;
1175
1190
  }
1191
+ log.info('TR: steps before applying delete', [...newTr.steps]);
1176
1192
  // First apply the deleted range and update the insert slice to not include content that was deleted,
1177
1193
  // eg partial nodes in an open-ended slice
1178
- const { deleteMap, mergedInsertPos, newSliceContent } = deleteAndMergeSplitNodes(fromA, toA, undefined, oldState.doc, newTr, oldState.schema, attrs, slice);
1179
- log.info('TR: new steps after applying delete', [...newTr.steps]);
1180
- const adjustedInsertPos = mergedInsertPos !== null && mergedInsertPos !== void 0 ? mergedInsertPos : deleteMap.map(toA);
1194
+ const { deleteMap, newSliceContent, steps: deleteSteps, } = deleteAndMergeSplitNodes(fromA, toA, undefined, oldState.doc, newTr, oldState.schema, attrs, slice);
1195
+ changeSteps.push(...deleteSteps);
1196
+ log.info('TR: steps after applying delete', [...newTr.steps]);
1197
+ log.info('DELETE STEPS: ', changeSteps);
1198
+ const adjustedInsertPos = deleteMap.map(toA);
1181
1199
  if (newSliceContent.size > 0) {
1182
1200
  log.info('newSliceContent', newSliceContent);
1183
1201
  // Since deleteAndMergeSplitBlockNodes modified the slice to not to contain any merged nodes,
1184
1202
  // the sides should be equal. TODO can they be other than 0?
1185
1203
  const openStart = slice.openStart !== slice.openEnd ? 0 : slice.openStart;
1186
1204
  const openEnd = slice.openStart !== slice.openEnd ? 0 : slice.openEnd;
1187
- const insertedSlice = new Slice(setFragmentAsInserted(newSliceContent, createNewInsertAttrs(attrs), oldState.schema), openStart, openEnd);
1188
- const newStep = new ReplaceStep(adjustedInsertPos, adjustedInsertPos, insertedSlice);
1205
+ changeSteps.push({
1206
+ type: 'insert-slice',
1207
+ from: adjustedInsertPos,
1208
+ to: adjustedInsertPos,
1209
+ slice: new prosemirrorModel.Slice(setFragmentAsInserted(newSliceContent, createNewInsertAttrs(attrs), oldState.schema), openStart, openEnd),
1210
+ });
1211
+ }
1212
+ else {
1213
+ // Incase only deletion was applied, check whether tracked marks around deleted content can be merged
1214
+ mergeTrackedMarks(adjustedInsertPos, newTr.doc, newTr, oldState.schema);
1215
+ selectionPos = fromA;
1216
+ }
1217
+ });
1218
+ return [changeSteps, selectionPos];
1219
+ }
1220
+
1221
+ /*!
1222
+ * © 2021 Atypon Systems LLC
1223
+ *
1224
+ * Licensed under the Apache License, Version 2.0 (the "License");
1225
+ * you may not use this file except in compliance with the License.
1226
+ * You may obtain a copy of the License at
1227
+ *
1228
+ * http://www.apache.org/licenses/LICENSE-2.0
1229
+ *
1230
+ * Unless required by applicable law or agreed to in writing, software
1231
+ * distributed under the License is distributed on an "AS IS" BASIS,
1232
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1233
+ * See the License for the specific language governing permissions and
1234
+ * limitations under the License.
1235
+ */
1236
+ /**
1237
+ * Deletes inserted text directly, otherwise wraps it with tracked_delete mark
1238
+ *
1239
+ * This would work for general inline nodes too, but since node marks don't work properly
1240
+ * with Yjs, attributes are used instead.
1241
+ * @param node
1242
+ * @param pos
1243
+ * @param newTr
1244
+ * @param schema
1245
+ * @param deleteAttrs
1246
+ * @param from
1247
+ * @param to
1248
+ */
1249
+ function deleteTextIfInserted(node, pos, newTr, schema, deleteAttrs, from, to) {
1250
+ const start = from ? Math.max(pos, from) : pos;
1251
+ const nodeEnd = pos + node.nodeSize;
1252
+ const end = to ? Math.min(nodeEnd, to) : nodeEnd;
1253
+ if (node.marks.find((m) => m.type === schema.marks.tracked_insert)) {
1254
+ // Math.max(pos, from) is for picking always the start of the node,
1255
+ // not the start of the change (which might span multiple nodes).
1256
+ // Pos can be less than from as nodesBetween iterates through all nodes starting from the top block node
1257
+ newTr.replaceWith(start, end, prosemirrorModel.Fragment.empty);
1258
+ return start;
1259
+ }
1260
+ else {
1261
+ const leftNode = newTr.doc.resolve(start).nodeBefore;
1262
+ const leftMarks = getMergeableMarkTrackedAttrs(leftNode, deleteAttrs, schema);
1263
+ const rightNode = newTr.doc.resolve(end).nodeAfter;
1264
+ const rightMarks = getMergeableMarkTrackedAttrs(rightNode, deleteAttrs, schema);
1265
+ const fromStartOfMark = start - (leftNode && leftMarks ? leftNode.nodeSize : 0);
1266
+ const toEndOfMark = end + (rightNode && rightMarks ? rightNode.nodeSize : 0);
1267
+ 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);
1268
+ const dataTracked = addTrackIdIfDoesntExist({
1269
+ ...leftMarks,
1270
+ ...rightMarks,
1271
+ ...deleteAttrs,
1272
+ createdAt,
1273
+ });
1274
+ newTr.addMark(fromStartOfMark, toEndOfMark, schema.marks.tracked_delete.create({
1275
+ dataTracked,
1276
+ }));
1277
+ return toEndOfMark;
1278
+ }
1279
+ }
1280
+
1281
+ /*!
1282
+ * © 2021 Atypon Systems LLC
1283
+ *
1284
+ * Licensed under the Apache License, Version 2.0 (the "License");
1285
+ * you may not use this file except in compliance with the License.
1286
+ * You may obtain a copy of the License at
1287
+ *
1288
+ * http://www.apache.org/licenses/LICENSE-2.0
1289
+ *
1290
+ * Unless required by applicable law or agreed to in writing, software
1291
+ * distributed under the License is distributed on an "AS IS" BASIS,
1292
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1293
+ * See the License for the specific language governing permissions and
1294
+ * limitations under the License.
1295
+ */
1296
+ function processChangeSteps(changes, startPos, newTr, emptyAttrs, schema) {
1297
+ const mapping = new prosemirrorTransform.Mapping();
1298
+ const deleteAttrs = createNewDeleteAttrs(emptyAttrs);
1299
+ let selectionPos = startPos;
1300
+ // @TODO add custom handler / condition?
1301
+ changes.forEach((c) => {
1302
+ let step = newTr.steps[newTr.steps.length - 1];
1303
+ log.info('process change: ', c);
1304
+ // const handled = customStepHandler(changes, newTr, emptyAttrs) // ChangeStep[] | undefined
1305
+ if (c.type === 'delete-node') {
1306
+ const dataTracked = c.node.attrs.dataTracked;
1307
+ const wasInsertedBySameUser = (dataTracked === null || dataTracked === void 0 ? void 0 : dataTracked.operation) === exports.CHANGE_OPERATION.insert &&
1308
+ dataTracked.authorID === emptyAttrs.authorID;
1309
+ if (wasInsertedBySameUser) {
1310
+ deleteNode(c.node, mapping.map(c.pos), newTr);
1311
+ const newestStep = newTr.steps[newTr.steps.length - 1];
1312
+ if (step !== newestStep) {
1313
+ mapping.appendMap(newestStep.getMap());
1314
+ step = newestStep;
1315
+ }
1316
+ mergeTrackedMarks(mapping.map(c.pos), newTr.doc, newTr, schema);
1317
+ }
1318
+ else {
1319
+ const attrs = {
1320
+ ...c.node.attrs,
1321
+ dataTracked: addTrackIdIfDoesntExist(deleteAttrs),
1322
+ };
1323
+ newTr.setNodeMarkup(mapping.map(c.pos), undefined, attrs, c.node.marks);
1324
+ }
1325
+ }
1326
+ else if (c.type === 'delete-text') {
1327
+ const from = mapping.map(c.from, -1);
1328
+ const to = mapping.map(c.to, 1);
1329
+ const node = newTr.doc.nodeAt(mapping.map(c.pos));
1330
+ if (node === null || node === void 0 ? void 0 : node.marks.find((m) => m.type === schema.marks.tracked_insert)) {
1331
+ newTr.replaceWith(from, to, prosemirrorModel.Fragment.empty);
1332
+ mergeTrackedMarks(from, newTr.doc, newTr, schema);
1333
+ }
1334
+ else {
1335
+ const leftNode = newTr.doc.resolve(from).nodeBefore;
1336
+ const leftMarks = getMergeableMarkTrackedAttrs(leftNode, deleteAttrs, schema);
1337
+ const rightNode = newTr.doc.resolve(to).nodeAfter;
1338
+ const rightMarks = getMergeableMarkTrackedAttrs(rightNode, deleteAttrs, schema);
1339
+ const fromStartOfMark = from - (leftNode && leftMarks ? leftNode.nodeSize : 0);
1340
+ const toEndOfMark = to + (rightNode && rightMarks ? rightNode.nodeSize : 0);
1341
+ const dataTracked = addTrackIdIfDoesntExist({
1342
+ ...leftMarks,
1343
+ ...rightMarks,
1344
+ ...deleteAttrs,
1345
+ });
1346
+ newTr.addMark(fromStartOfMark, toEndOfMark, schema.marks.tracked_delete.create({
1347
+ dataTracked,
1348
+ }));
1349
+ }
1350
+ }
1351
+ else if (c.type === 'merge-fragment') {
1352
+ let insertPos = mapping.map(c.mergePos);
1353
+ // The default insert position for block nodes is either the start of the merged content or the end.
1354
+ // Incase text was merged, this must be updated as the start or end of the node doesn't map to the
1355
+ // actual position of the merge. Currently the inserted content is inserted at the start or end
1356
+ // of the merged content, TODO reverse the start/end when end/start token?
1357
+ if (c.node.isText) {
1358
+ // When merging text we must delete text in the same go as well, as the from/to boundary goes through
1359
+ // the text node.
1360
+ insertPos = deleteTextIfInserted(c.node, mapping.map(c.pos), newTr, schema, deleteAttrs, mapping.map(c.from), mapping.map(c.to));
1361
+ const newestStep = newTr.steps[newTr.steps.length - 1];
1362
+ if (step !== newestStep) {
1363
+ mapping.appendMap(newestStep.getMap());
1364
+ step = newestStep;
1365
+ }
1366
+ }
1367
+ if (c.fragment.size > 0) {
1368
+ newTr.insert(insertPos, c.fragment);
1369
+ }
1370
+ }
1371
+ else if (c.type === 'insert-slice') {
1372
+ const newStep = new prosemirrorTransform.ReplaceStep(mapping.map(c.from), mapping.map(c.to), c.slice, false);
1189
1373
  const stepResult = newTr.maybeStep(newStep);
1190
1374
  if (stepResult.failed) {
1191
1375
  log.error(`insert ReplaceStep failed: "${stepResult.failed}"`, newStep);
1192
1376
  return;
1193
1377
  }
1194
- log.info('new steps after applying insert', [...newTr.steps]);
1195
- mergeTrackedMarks(adjustedInsertPos, newTr.doc, newTr, oldState.schema);
1196
- mergeTrackedMarks(adjustedInsertPos + insertedSlice.size, newTr.doc, newTr, oldState.schema);
1197
- selectionPos = adjustedInsertPos + insertedSlice.size;
1378
+ mergeTrackedMarks(mapping.map(c.from), newTr.doc, newTr, schema);
1379
+ mergeTrackedMarks(mapping.map(c.to), newTr.doc, newTr, schema);
1380
+ selectionPos = mapping.map(c.to) + c.slice.size;
1198
1381
  }
1199
- else {
1200
- // Incase only deletion was applied, check whether tracked marks around deleted content can be merged
1201
- mergeTrackedMarks(adjustedInsertPos, newTr.doc, newTr, oldState.schema);
1202
- selectionPos = fromA;
1382
+ else if (c.type === 'update-node-attrs') {
1383
+ const oldDataTracked = c.oldAttrs.dataTracked;
1384
+ const oldAttrs = (oldDataTracked === null || oldDataTracked === void 0 ? void 0 : oldDataTracked.operation) === exports.CHANGE_OPERATION.set_node_attributes
1385
+ ? oldDataTracked.oldAttrs
1386
+ : c.oldAttrs;
1387
+ const dataTracked = addTrackIdIfDoesntExist({
1388
+ ...oldDataTracked,
1389
+ oldAttrs,
1390
+ ...emptyAttrs,
1391
+ operation: exports.CHANGE_OPERATION.set_node_attributes,
1392
+ });
1393
+ newTr.setNodeMarkup(mapping.map(c.pos), undefined, { ...c.newAttrs, dataTracked });
1394
+ }
1395
+ const newestStep = newTr.steps[newTr.steps.length - 1];
1396
+ if (step !== newestStep) {
1397
+ mapping.appendMap(newestStep.getMap());
1398
+ }
1399
+ });
1400
+ return [mapping, selectionPos];
1401
+ }
1402
+
1403
+ /*!
1404
+ * © 2021 Atypon Systems LLC
1405
+ *
1406
+ * Licensed under the Apache License, Version 2.0 (the "License");
1407
+ * you may not use this file except in compliance with the License.
1408
+ * You may obtain a copy of the License at
1409
+ *
1410
+ * http://www.apache.org/licenses/LICENSE-2.0
1411
+ *
1412
+ * Unless required by applicable law or agreed to in writing, software
1413
+ * distributed under the License is distributed on an "AS IS" BASIS,
1414
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1415
+ * See the License for the specific language governing permissions and
1416
+ * limitations under the License.
1417
+ */
1418
+ function matchInserted(inDeleted, deleted, inserted, newTr, schema) {
1419
+ var _a;
1420
+ for (let i = 0;; i += 1) {
1421
+ if (inserted.childCount === i)
1422
+ return [inDeleted, deleted];
1423
+ const child = inserted.child(i);
1424
+ // @ts-ignore
1425
+ let adjDeleted = deleted.find((d) => (d.type === 'delete-text' && d.to === inDeleted) ||
1426
+ (d.type === 'delete-node' && d.nodeEnd === inDeleted));
1427
+ if (child.type !== ((_a = adjDeleted === null || adjDeleted === void 0 ? void 0 : adjDeleted.node) === null || _a === void 0 ? void 0 : _a.type)) {
1428
+ return [inDeleted, deleted];
1429
+ }
1430
+ else if (child.isText && (adjDeleted === null || adjDeleted === void 0 ? void 0 : adjDeleted.node)) {
1431
+ adjDeleted = adjDeleted;
1432
+ const { pos, from, to, node } = adjDeleted;
1433
+ let j = 0, d = from - pos, maxSteps = Math.max(pos, from) - to;
1434
+ // Match text inside the inserted text node to the deleted text node
1435
+ for (; maxSteps !== j && child.text[j] === node.text[d]; j += 1, d += 1) {
1436
+ inDeleted -= 1;
1437
+ }
1438
+ // this is needed incase diffing tr.doc
1439
+ // deleted.push({
1440
+ // pos: pos,
1441
+ // type: 'update-node-attrs',
1442
+ // // Should check the attrs for equality in fixInconsistentChanges? to remove dataTracked completely
1443
+ // oldAttrs: adjDeleted.node.attrs || {},
1444
+ // newAttrs: child.attrs || {},
1445
+ // })
1446
+ if (maxSteps !== j) {
1447
+ deleted.push({
1448
+ pos,
1449
+ from: Math.max(pos, from) + j,
1450
+ to,
1451
+ type: 'delete-text',
1452
+ node,
1453
+ });
1454
+ }
1455
+ return [inDeleted, deleted.filter((d) => d !== adjDeleted)];
1456
+ }
1457
+ else if (child.content.size > 0 || (adjDeleted === null || adjDeleted === void 0 ? void 0 : adjDeleted.node.content.size) > 0) {
1458
+ // Move the inDeleted inside the block node's boundary
1459
+ return matchInserted(inDeleted - 1, deleted.filter((d) => d !== adjDeleted), child.content);
1460
+ }
1461
+ deleted.push({
1462
+ pos: adjDeleted.pos,
1463
+ type: 'update-node-attrs',
1464
+ // Should check the attrs for equality in fixInconsistentChanges? to remove dataTracked completely
1465
+ oldAttrs: adjDeleted.node.attrs || {},
1466
+ newAttrs: child.attrs || {},
1467
+ });
1468
+ deleted = deleted.filter((d) => d !== adjDeleted);
1469
+ inDeleted -= child.nodeSize;
1470
+ }
1471
+ }
1472
+ function diffChangeSteps(deleted, inserted, newTr, schema) {
1473
+ const updated = [];
1474
+ let updatedDeleted = [...deleted];
1475
+ inserted.forEach((ins) => {
1476
+ const [inDeleted, updatedDel] = matchInserted(ins.from, updatedDeleted, ins.slice.content);
1477
+ if (inDeleted === ins.from) {
1478
+ updated.push(ins);
1479
+ return;
1480
+ }
1481
+ updatedDeleted = updatedDel;
1482
+ const newInsertedA = ins.slice.content.cut(ins.from - inDeleted);
1483
+ const newInsertedB = ins.slice.content.cut(ins.from - inDeleted + 1);
1484
+ // Super hax to cut over block node boundaries in the inserted fragment
1485
+ const newInserted = newInsertedA.size === newInsertedB.size + 2 ? newInsertedB : newInsertedA;
1486
+ if (newInserted.size > 0) {
1487
+ updated.push({
1488
+ ...ins,
1489
+ slice: new prosemirrorModel.Slice(newInserted, ins.slice.openStart, ins.slice.openEnd),
1490
+ });
1203
1491
  }
1204
1492
  });
1205
- return selectionPos;
1493
+ return [...updatedDeleted, ...updated];
1206
1494
  }
1207
1495
 
1208
1496
  /**
@@ -1226,15 +1514,16 @@ const getSelectionStaticConstructor = (sel) => Object.getPrototypeOf(sel).constr
1226
1514
  * @param tr Original transaction
1227
1515
  * @param oldState State before transaction
1228
1516
  * @param newTr Transaction created from the new editor state
1229
- * @param userID User id
1517
+ * @param authorID User id
1230
1518
  * @returns newTr that inverts the initial tr and applies track attributes/marks
1231
1519
  */
1232
- function trackTransaction(tr, oldState, newTr, userID) {
1233
- var _a;
1520
+ function trackTransaction(tr, oldState, newTr, authorID) {
1234
1521
  const emptyAttrs = {
1235
- userID,
1522
+ authorID,
1523
+ reviewedByID: null,
1236
1524
  createdAt: tr.time,
1237
- status: CHANGE_STATUS.pending,
1525
+ updatedAt: tr.time,
1526
+ status: exports.CHANGE_STATUS.pending,
1238
1527
  };
1239
1528
  // Must use constructor.name instead of instanceof as aliasing prosemirror-state is a lot more
1240
1529
  // difficult than prosemirror-transform
@@ -1249,13 +1538,20 @@ function trackTransaction(tr, oldState, newTr, userID) {
1249
1538
  'This is probably an error with the library, please report back to maintainers with a reproduction if possible', newTr);
1250
1539
  return;
1251
1540
  }
1252
- else if (!(step instanceof ReplaceStep) && step.constructor.name === 'ReplaceStep') {
1541
+ else if (!(step instanceof prosemirrorTransform.ReplaceStep) && step.constructor.name === 'ReplaceStep') {
1253
1542
  console.error('@manuscripts/track-changes-plugin: Multiple prosemirror-transform packages imported, alias/dedupe them ' +
1254
1543
  'or instanceof checks fail as well as creating new steps');
1255
1544
  return;
1256
1545
  }
1257
- else if (step instanceof ReplaceStep) {
1258
- const selectionPos = trackReplaceStep(step, oldState, newTr, emptyAttrs);
1546
+ else if (step instanceof prosemirrorTransform.ReplaceStep) {
1547
+ let [steps, startPos] = trackReplaceStep(step, oldState, newTr, emptyAttrs);
1548
+ log.info('CHANGES: ', steps);
1549
+ // deleted and merged really...
1550
+ const deleted = steps.filter((s) => s.type !== 'insert-slice');
1551
+ const inserted = steps.filter((s) => s.type === 'insert-slice');
1552
+ steps = diffChangeSteps(deleted, inserted, newTr, oldState.schema);
1553
+ log.info('DIFFED STEPS: ', steps);
1554
+ const [mapping, selectionPos] = processChangeSteps(steps, startPos, newTr, emptyAttrs, oldState.schema);
1259
1555
  if (!wasNodeSelection) {
1260
1556
  const sel = getSelectionStaticConstructor(tr.selection);
1261
1557
  // Use Selection.near to fix selections that point to a block node instead of inline content
@@ -1265,11 +1561,17 @@ function trackTransaction(tr, oldState, newTr, userID) {
1265
1561
  newTr.setSelection(near);
1266
1562
  }
1267
1563
  }
1268
- else if (step instanceof ReplaceAroundStep) {
1269
- trackReplaceAroundStep(step, oldState, newTr, emptyAttrs);
1270
- // } else if (step instanceof AddMarkStep) {
1271
- // } else if (step instanceof RemoveMarkStep) {
1564
+ else if (step instanceof prosemirrorTransform.ReplaceAroundStep) {
1565
+ let steps = trackReplaceAroundStep(step, oldState, newTr, emptyAttrs);
1566
+ const deleted = steps.filter((s) => s.type !== 'insert-slice');
1567
+ const inserted = steps.filter((s) => s.type === 'insert-slice');
1568
+ log.info('INSERT STEPS: ', inserted);
1569
+ steps = diffChangeSteps(deleted, inserted, newTr, oldState.schema);
1570
+ log.info('DIFFED STEPS: ', steps);
1571
+ processChangeSteps(steps, tr.selection.from, newTr, emptyAttrs, oldState.schema);
1272
1572
  }
1573
+ // } else if (step instanceof AddMarkStep) {
1574
+ // } else if (step instanceof RemoveMarkStep) {
1273
1575
  // TODO: here we could check whether adjacent inserts & deletes cancel each other out.
1274
1576
  // However, this should not be done by diffing and only matching node or char by char instead since
1275
1577
  // it's A easier and B more intuitive to user.
@@ -1281,24 +1583,24 @@ function trackTransaction(tr, oldState, newTr, userID) {
1281
1583
  tr.getMeta('uiEvent') && newTr.setMeta('uiEvent', tr.getMeta('uiEvent'));
1282
1584
  });
1283
1585
  // This is kinda hacky solution at the moment to maintain NodeSelections over transactions
1284
- // These are required by at least cross-references that need it to activate the selector pop-up
1586
+ // These are required by at least cross-references and links to activate their selector pop-ups
1285
1587
  if (wasNodeSelection) {
1286
- const mappedPos = newTr.mapping.map(tr.selection.from);
1287
- const resPos = newTr.doc.resolve(mappedPos);
1288
- const nodePos = mappedPos - (((_a = resPos.nodeBefore) === null || _a === void 0 ? void 0 : _a.nodeSize) || 0);
1588
+ // And -1 here is necessary to keep the selection pointing at the start of the node
1589
+ // (or something, breaks with cross-references otherwise)
1590
+ const mappedPos = newTr.mapping.map(tr.selection.from, -1);
1289
1591
  const sel = getSelectionStaticConstructor(tr.selection);
1290
- newTr.setSelection(sel.create(newTr.doc, nodePos));
1592
+ newTr.setSelection(sel.create(newTr.doc, mappedPos));
1291
1593
  }
1292
1594
  log.info('NEW transaction', newTr);
1293
1595
  return newTr;
1294
1596
  }
1295
1597
 
1296
- var TrackChangesStatus;
1598
+ exports.TrackChangesStatus = void 0;
1297
1599
  (function (TrackChangesStatus) {
1298
1600
  TrackChangesStatus["enabled"] = "enabled";
1299
1601
  TrackChangesStatus["viewSnapshots"] = "view-snapshots";
1300
1602
  TrackChangesStatus["disabled"] = "disabled";
1301
- })(TrackChangesStatus || (TrackChangesStatus = {}));
1603
+ })(exports.TrackChangesStatus || (exports.TrackChangesStatus = {}));
1302
1604
 
1303
1605
  /*!
1304
1606
  * © 2021 Atypon Systems LLC
@@ -1315,12 +1617,7 @@ var TrackChangesStatus;
1315
1617
  * See the License for the specific language governing permissions and
1316
1618
  * limitations under the License.
1317
1619
  */
1318
- const trackChangesPluginKey = new PluginKey('track-changes');
1319
- // TODO remove
1320
- const infiniteLoopCounter = {
1321
- start: 0,
1322
- iters: 0,
1323
- };
1620
+ const trackChangesPluginKey = new prosemirrorState.PluginKey('track-changes');
1324
1621
  /**
1325
1622
  * The ProseMirror plugin needed to enable track-changes.
1326
1623
  *
@@ -1333,18 +1630,18 @@ const trackChangesPlugin = (opts = { userID: 'anonymous:Anonymous' }) => {
1333
1630
  if (debug) {
1334
1631
  enableDebug(true);
1335
1632
  }
1336
- return new Plugin({
1633
+ return new prosemirrorState.Plugin({
1337
1634
  key: trackChangesPluginKey,
1338
1635
  props: {
1339
1636
  editable(state) {
1340
1637
  var _a;
1341
- return ((_a = trackChangesPluginKey.getState(state)) === null || _a === void 0 ? void 0 : _a.status) !== TrackChangesStatus.viewSnapshots;
1638
+ return ((_a = trackChangesPluginKey.getState(state)) === null || _a === void 0 ? void 0 : _a.status) !== exports.TrackChangesStatus.viewSnapshots;
1342
1639
  },
1343
1640
  },
1344
1641
  state: {
1345
1642
  init(_config, state) {
1346
1643
  return {
1347
- status: TrackChangesStatus.enabled,
1644
+ status: exports.TrackChangesStatus.enabled,
1348
1645
  userID,
1349
1646
  changeSet: findChanges(state),
1350
1647
  };
@@ -1362,7 +1659,7 @@ const trackChangesPlugin = (opts = { userID: 'anonymous:Anonymous' }) => {
1362
1659
  changeSet: findChanges(newState),
1363
1660
  };
1364
1661
  }
1365
- else if (pluginState.status === TrackChangesStatus.disabled) {
1662
+ else if (pluginState.status === exports.TrackChangesStatus.disabled) {
1366
1663
  return { ...pluginState, changeSet: new ChangeSet() };
1367
1664
  }
1368
1665
  let { changeSet, ...rest } = pluginState;
@@ -1386,18 +1683,10 @@ const trackChangesPlugin = (opts = { userID: 'anonymous:Anonymous' }) => {
1386
1683
  appendTransaction(trs, oldState, newState) {
1387
1684
  const pluginState = trackChangesPluginKey.getState(newState);
1388
1685
  if (!pluginState ||
1389
- pluginState.status === TrackChangesStatus.disabled ||
1686
+ pluginState.status === exports.TrackChangesStatus.disabled ||
1390
1687
  !(editorView === null || editorView === void 0 ? void 0 : editorView.editable)) {
1391
1688
  return null;
1392
1689
  }
1393
- if (infiniteLoopCounter.start < Date.now() - 10000) {
1394
- infiniteLoopCounter.start = Date.now();
1395
- infiniteLoopCounter.iters = 0;
1396
- }
1397
- if (infiniteLoopCounter.iters >= 100) {
1398
- console.error('Detected probable infinite loop in track changes!');
1399
- return null;
1400
- }
1401
1690
  const { userID, changeSet } = pluginState;
1402
1691
  let createdTr = newState.tr, docChanged = false;
1403
1692
  log.info('TRS', trs);
@@ -1408,7 +1697,6 @@ const trackChangesPlugin = (opts = { userID: 'anonymous:Anonymous' }) => {
1408
1697
  (wasAppended && getAction(wasAppended, TrackChangesAction.skipTrack));
1409
1698
  if (tr.docChanged && !skipMetaUsed && !skipTrackUsed && !tr.getMeta('history$')) {
1410
1699
  createdTr = trackTransaction(tr, oldState, createdTr, userID);
1411
- infiniteLoopCounter.iters += 1;
1412
1700
  }
1413
1701
  docChanged = docChanged || tr.docChanged;
1414
1702
  const setChangeStatuses = getAction(tr, TrackChangesAction.setChangeStatuses);
@@ -1417,7 +1705,7 @@ const trackChangesPlugin = (opts = { userID: 'anonymous:Anonymous' }) => {
1417
1705
  ids.forEach((changeId) => {
1418
1706
  const change = changeSet === null || changeSet === void 0 ? void 0 : changeSet.get(changeId);
1419
1707
  if (change) {
1420
- createdTr = updateChangeAttrs(createdTr, change, { status }, oldState.schema);
1708
+ createdTr = updateChangeAttrs(createdTr, change, { status, reviewedByID: userID }, oldState.schema);
1421
1709
  setAction(createdTr, TrackChangesAction.updateChanges, [change.id]);
1422
1710
  }
1423
1711
  });
@@ -1442,21 +1730,6 @@ const trackChangesPlugin = (opts = { userID: 'anonymous:Anonymous' }) => {
1442
1730
  });
1443
1731
  };
1444
1732
 
1445
- /*!
1446
- * © 2021 Atypon Systems LLC
1447
- *
1448
- * Licensed under the Apache License, Version 2.0 (the "License");
1449
- * you may not use this file except in compliance with the License.
1450
- * You may obtain a copy of the License at
1451
- *
1452
- * http://www.apache.org/licenses/LICENSE-2.0
1453
- *
1454
- * Unless required by applicable law or agreed to in writing, software
1455
- * distributed under the License is distributed on an "AS IS" BASIS,
1456
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1457
- * See the License for the specific language governing permissions and
1458
- * limitations under the License.
1459
- */
1460
1733
  /**
1461
1734
  * Sets track-changes plugin's status to any of: 'enabled' 'disabled' 'viewSnapshots'. Passing undefined will
1462
1735
  * set 'enabled' status to 'disabled' and 'disabled' | 'viewSnapshots' status to 'enabled'.
@@ -1473,9 +1746,9 @@ const setTrackingStatus = (status) => (state, dispatch) => {
1473
1746
  let newStatus = status;
1474
1747
  if (newStatus === undefined) {
1475
1748
  newStatus =
1476
- currentStatus === TrackChangesStatus.enabled
1477
- ? TrackChangesStatus.disabled
1478
- : TrackChangesStatus.enabled;
1749
+ currentStatus === exports.TrackChangesStatus.enabled
1750
+ ? exports.TrackChangesStatus.disabled
1751
+ : exports.TrackChangesStatus.enabled;
1479
1752
  }
1480
1753
  dispatch && dispatch(setAction(state.tr, TrackChangesAction.setPluginStatus, newStatus));
1481
1754
  return true;
@@ -1544,4 +1817,9 @@ var commands = /*#__PURE__*/Object.freeze({
1544
1817
  setParagraphTestAttribute: setParagraphTestAttribute
1545
1818
  });
1546
1819
 
1547
- export { CHANGE_OPERATION, CHANGE_STATUS, ChangeSet, TrackChangesAction, TrackChangesStatus, enableDebug, getAction, setAction, trackChangesPlugin, trackChangesPluginKey, commands as trackCommands };
1820
+ exports.ChangeSet = ChangeSet;
1821
+ exports.enableDebug = enableDebug;
1822
+ exports.skipTracking = skipTracking;
1823
+ exports.trackChangesPlugin = trackChangesPlugin;
1824
+ exports.trackChangesPluginKey = trackChangesPluginKey;
1825
+ exports.trackCommands = commands;