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