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