@manuscripts/track-changes-plugin 0.2.0 → 0.4.1

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