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