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