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