@manuscripts/track-changes-plugin 0.1.1 → 0.4.0
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 +20 -45
- package/dist/ChangeSet.d.ts +3 -1
- package/dist/actions.d.ts +8 -1
- package/dist/change-steps/diffChangeSteps.d.ts +21 -0
- package/dist/change-steps/processChangeSteps.d.ts +21 -0
- package/dist/{track → changes}/applyChanges.d.ts +0 -0
- package/dist/{track → changes}/findChanges.d.ts +0 -0
- package/dist/{track → changes}/fixInconsistentChanges.d.ts +0 -0
- package/dist/{track → changes}/updateChangeAttrs.d.ts +0 -0
- package/dist/commands.d.ts +1 -1
- package/dist/{track/node-utils.d.ts → compute/nodeHelpers.d.ts} +4 -11
- package/dist/{track/steps → compute}/setFragmentAsInserted.d.ts +1 -1
- package/dist/compute/splitSliceIntoMergedParts.d.ts +41 -0
- package/dist/{index.es.js → index.cjs} +658 -380
- package/dist/index.d.ts +1 -1
- package/dist/index.js +663 -412
- package/dist/{track/steps → mutate}/deleteAndMergeSplitNodes.d.ts +4 -3
- package/dist/{track → mutate}/deleteNode.d.ts +9 -0
- package/dist/mutate/deleteText.d.ts +32 -0
- package/dist/{track → mutate}/mergeNode.d.ts +1 -1
- package/dist/{track/steps → mutate}/mergeTrackedMarks.d.ts +0 -0
- package/dist/{track/steps → steps}/trackReplaceAroundStep.d.ts +3 -2
- package/dist/{track/steps → steps}/trackReplaceStep.d.ts +3 -2
- package/dist/{track → steps}/trackTransaction.d.ts +2 -2
- package/dist/types/change.d.ts +22 -11
- package/dist/types/step.d.ts +52 -0
- package/dist/{track/steps → utils}/track-utils.d.ts +1 -2
- package/package.json +6 -6
- package/dist/types/editor.d.ts +0 -23
|
@@ -1,7 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var prosemirrorState = require('prosemirror-state');
|
|
6
|
+
var debug = require('debug');
|
|
7
|
+
var prosemirrorTransform = require('prosemirror-transform');
|
|
8
|
+
var prosemirrorModel = require('prosemirror-model');
|
|
9
|
+
|
|
10
|
+
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
|
|
11
|
+
|
|
12
|
+
var debug__default = /*#__PURE__*/_interopDefaultLegacy(debug);
|
|
5
13
|
|
|
6
14
|
var TrackChangesAction;
|
|
7
15
|
(function (TrackChangesAction) {
|
|
@@ -30,7 +38,14 @@ function getAction(tr, action) {
|
|
|
30
38
|
*/
|
|
31
39
|
function setAction(tr, action, payload) {
|
|
32
40
|
return tr.setMeta(action, payload);
|
|
33
|
-
}
|
|
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);
|
|
34
49
|
|
|
35
50
|
/******************************************************************************
|
|
36
51
|
Copyright (c) Microsoft Corporation.
|
|
@@ -75,22 +90,22 @@ function __classPrivateFieldSet(receiver, state, value, kind, f) {
|
|
|
75
90
|
* See the License for the specific language governing permissions and
|
|
76
91
|
* limitations under the License.
|
|
77
92
|
*/
|
|
78
|
-
|
|
93
|
+
exports.CHANGE_OPERATION = void 0;
|
|
79
94
|
(function (CHANGE_OPERATION) {
|
|
80
95
|
CHANGE_OPERATION["insert"] = "insert";
|
|
81
96
|
CHANGE_OPERATION["delete"] = "delete";
|
|
82
|
-
CHANGE_OPERATION["set_node_attributes"] = "
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
})(CHANGE_OPERATION || (CHANGE_OPERATION = {}));
|
|
88
|
-
|
|
97
|
+
CHANGE_OPERATION["set_node_attributes"] = "set_attrs";
|
|
98
|
+
// wrap_with_node = 'wrap_with_node',
|
|
99
|
+
// unwrap_from_node = 'unwrap_from_node',
|
|
100
|
+
// add_mark = 'add_mark',
|
|
101
|
+
// remove_mark = 'remove_mark',
|
|
102
|
+
})(exports.CHANGE_OPERATION || (exports.CHANGE_OPERATION = {}));
|
|
103
|
+
exports.CHANGE_STATUS = void 0;
|
|
89
104
|
(function (CHANGE_STATUS) {
|
|
90
105
|
CHANGE_STATUS["accepted"] = "accepted";
|
|
91
106
|
CHANGE_STATUS["rejected"] = "rejected";
|
|
92
107
|
CHANGE_STATUS["pending"] = "pending";
|
|
93
|
-
})(CHANGE_STATUS || (CHANGE_STATUS = {}));
|
|
108
|
+
})(exports.CHANGE_STATUS || (exports.CHANGE_STATUS = {}));
|
|
94
109
|
|
|
95
110
|
/*!
|
|
96
111
|
* © 2021 Atypon Systems LLC
|
|
@@ -107,7 +122,7 @@ var CHANGE_STATUS;
|
|
|
107
122
|
* See the License for the specific language governing permissions and
|
|
108
123
|
* limitations under the License.
|
|
109
124
|
*/
|
|
110
|
-
const logger =
|
|
125
|
+
const logger = debug__default["default"]('track');
|
|
111
126
|
const log = {
|
|
112
127
|
info(str, obj) {
|
|
113
128
|
if (obj) {
|
|
@@ -140,10 +155,10 @@ const log = {
|
|
|
140
155
|
*/
|
|
141
156
|
const enableDebug = (enabled) => {
|
|
142
157
|
if (enabled) {
|
|
143
|
-
|
|
158
|
+
debug__default["default"].enable('track');
|
|
144
159
|
}
|
|
145
160
|
else {
|
|
146
|
-
|
|
161
|
+
debug__default["default"].disable();
|
|
147
162
|
}
|
|
148
163
|
};
|
|
149
164
|
|
|
@@ -184,16 +199,13 @@ class ChangeSet {
|
|
|
184
199
|
rootNodes.push(currentNodeChange);
|
|
185
200
|
currentNodeChange = undefined;
|
|
186
201
|
}
|
|
187
|
-
if (
|
|
202
|
+
if (currentNodeChange && c.from < currentNodeChange.to) {
|
|
188
203
|
currentNodeChange.children.push(c);
|
|
189
204
|
}
|
|
190
205
|
else if (c.type === 'node-change') {
|
|
191
206
|
currentNodeChange = { ...c, children: [] };
|
|
192
207
|
}
|
|
193
|
-
else
|
|
194
|
-
currentNodeChange.children.push(c);
|
|
195
|
-
}
|
|
196
|
-
else if (c.type === 'text-change') {
|
|
208
|
+
else {
|
|
197
209
|
rootNodes.push(c);
|
|
198
210
|
}
|
|
199
211
|
});
|
|
@@ -203,13 +215,13 @@ class ChangeSet {
|
|
|
203
215
|
return rootNodes;
|
|
204
216
|
}
|
|
205
217
|
get pending() {
|
|
206
|
-
return this.changeTree.filter((c) => c.attrs.status === CHANGE_STATUS.pending);
|
|
218
|
+
return this.changeTree.filter((c) => c.attrs.status === exports.CHANGE_STATUS.pending);
|
|
207
219
|
}
|
|
208
220
|
get accepted() {
|
|
209
|
-
return this.changeTree.filter((c) => c.attrs.status === CHANGE_STATUS.accepted);
|
|
221
|
+
return this.changeTree.filter((c) => c.attrs.status === exports.CHANGE_STATUS.accepted);
|
|
210
222
|
}
|
|
211
223
|
get rejected() {
|
|
212
|
-
return this.changeTree.filter((c) => c.attrs.status === CHANGE_STATUS.rejected);
|
|
224
|
+
return this.changeTree.filter((c) => c.attrs.status === exports.CHANGE_STATUS.rejected);
|
|
213
225
|
}
|
|
214
226
|
get textChanges() {
|
|
215
227
|
return this.changes.filter((c) => c.type === 'text-change');
|
|
@@ -217,6 +229,9 @@ class ChangeSet {
|
|
|
217
229
|
get nodeChanges() {
|
|
218
230
|
return this.changes.filter((c) => c.type === 'node-change');
|
|
219
231
|
}
|
|
232
|
+
get nodeAttrChanges() {
|
|
233
|
+
return this.changes.filter((c) => c.type === 'node-attr-change');
|
|
234
|
+
}
|
|
220
235
|
get isEmpty() {
|
|
221
236
|
return __classPrivateFieldGet(this, _ChangeSet_changes, "f").length === 0;
|
|
222
237
|
}
|
|
@@ -262,9 +277,7 @@ class ChangeSet {
|
|
|
262
277
|
* @param change
|
|
263
278
|
*/
|
|
264
279
|
static shouldNotDelete(change) {
|
|
265
|
-
|
|
266
|
-
return ((operation === CHANGE_OPERATION.insert && status === CHANGE_STATUS.accepted) ||
|
|
267
|
-
(operation === CHANGE_OPERATION.delete && status === CHANGE_STATUS.rejected));
|
|
280
|
+
return !ChangeSet.shouldDeleteChange(change);
|
|
268
281
|
}
|
|
269
282
|
/**
|
|
270
283
|
* Determines whether a change should be deleted when applying it to the document.
|
|
@@ -272,8 +285,8 @@ class ChangeSet {
|
|
|
272
285
|
*/
|
|
273
286
|
static shouldDeleteChange(change) {
|
|
274
287
|
const { status, operation } = change.attrs;
|
|
275
|
-
return ((operation === CHANGE_OPERATION.insert && status === CHANGE_STATUS.rejected) ||
|
|
276
|
-
(operation === CHANGE_OPERATION.delete && status === CHANGE_STATUS.accepted));
|
|
288
|
+
return ((operation === exports.CHANGE_OPERATION.insert && status === exports.CHANGE_STATUS.rejected) ||
|
|
289
|
+
(operation === exports.CHANGE_OPERATION.delete && status === exports.CHANGE_STATUS.accepted));
|
|
277
290
|
}
|
|
278
291
|
/**
|
|
279
292
|
* Checks whether change attributes contain all TrackedAttrs keys with non-undefined values
|
|
@@ -283,10 +296,22 @@ class ChangeSet {
|
|
|
283
296
|
if ('attrs' in attrs) {
|
|
284
297
|
log.warn('passed "attrs" as property to isValidTrackedAttrs(attrs)', attrs);
|
|
285
298
|
}
|
|
286
|
-
const trackedKeys = [
|
|
287
|
-
|
|
299
|
+
const trackedKeys = [
|
|
300
|
+
'id',
|
|
301
|
+
'authorID',
|
|
302
|
+
'operation',
|
|
303
|
+
'status',
|
|
304
|
+
'createdAt',
|
|
305
|
+
'updatedAt',
|
|
306
|
+
];
|
|
307
|
+
// reviewedByID is set optional since either ProseMirror or Yjs doesn't like persisting null values inside attributes objects
|
|
308
|
+
// So it can be either omitted completely or at least be null or string
|
|
309
|
+
const optionalKeys = ['reviewedByID'];
|
|
310
|
+
const entries = Object.entries(attrs).filter(([key, val]) => trackedKeys.includes(key));
|
|
311
|
+
const optionalEntries = Object.entries(attrs).filter(([key, val]) => optionalKeys.includes(key));
|
|
288
312
|
return (entries.length === trackedKeys.length &&
|
|
289
313
|
entries.every(([key, val]) => trackedKeys.includes(key) && val !== undefined) &&
|
|
314
|
+
optionalEntries.every(([key, val]) => optionalKeys.includes(key) && val !== undefined) &&
|
|
290
315
|
(attrs.id || '').length > 0 // Changes created with undefined id have '' as placeholder
|
|
291
316
|
);
|
|
292
317
|
}
|
|
@@ -296,9 +321,89 @@ class ChangeSet {
|
|
|
296
321
|
static isNodeChange(change) {
|
|
297
322
|
return change.type === 'node-change';
|
|
298
323
|
}
|
|
324
|
+
static isNodeAttrChange(change) {
|
|
325
|
+
return change.type === 'node-attr-change';
|
|
326
|
+
}
|
|
299
327
|
}
|
|
300
328
|
_ChangeSet_changes = new WeakMap();
|
|
301
329
|
|
|
330
|
+
/*!
|
|
331
|
+
* © 2021 Atypon Systems LLC
|
|
332
|
+
*
|
|
333
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
334
|
+
* you may not use this file except in compliance with the License.
|
|
335
|
+
* You may obtain a copy of the License at
|
|
336
|
+
*
|
|
337
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
338
|
+
*
|
|
339
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
340
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
341
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
342
|
+
* See the License for the specific language governing permissions and
|
|
343
|
+
* limitations under the License.
|
|
344
|
+
*/
|
|
345
|
+
function uuidv4() {
|
|
346
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
|
347
|
+
const r = (Math.random() * 16) | 0, v = c == 'x' ? r : (r & 0x3) | 0x8;
|
|
348
|
+
return v.toString(16);
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function addTrackIdIfDoesntExist(attrs) {
|
|
353
|
+
if (!attrs.id) {
|
|
354
|
+
return {
|
|
355
|
+
id: uuidv4(),
|
|
356
|
+
...attrs,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
return attrs;
|
|
360
|
+
}
|
|
361
|
+
function getInlineNodeTrackedMarkData(node, schema) {
|
|
362
|
+
if (!node || !node.isInline) {
|
|
363
|
+
return undefined;
|
|
364
|
+
}
|
|
365
|
+
const marksTrackedData = [];
|
|
366
|
+
node.marks.forEach((mark) => {
|
|
367
|
+
if (mark.type === schema.marks.tracked_insert || mark.type === schema.marks.tracked_delete) {
|
|
368
|
+
const operation = mark.type === schema.marks.tracked_insert
|
|
369
|
+
? exports.CHANGE_OPERATION.insert
|
|
370
|
+
: exports.CHANGE_OPERATION.delete;
|
|
371
|
+
marksTrackedData.push({ ...mark.attrs.dataTracked, operation });
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
if (marksTrackedData.length > 1) {
|
|
375
|
+
log.warn('inline node with more than 1 of tracked marks', marksTrackedData);
|
|
376
|
+
}
|
|
377
|
+
return marksTrackedData[0] || undefined;
|
|
378
|
+
}
|
|
379
|
+
function getNodeTrackedData(node, schema) {
|
|
380
|
+
return !node
|
|
381
|
+
? undefined
|
|
382
|
+
: node.isText
|
|
383
|
+
? getInlineNodeTrackedMarkData(node, schema)
|
|
384
|
+
: node.attrs.dataTracked;
|
|
385
|
+
}
|
|
386
|
+
function equalMarks(n1, n2) {
|
|
387
|
+
return (n1.marks.length === n2.marks.length &&
|
|
388
|
+
n1.marks.every((mark) => n1.marks.find((m) => m.type === mark.type)));
|
|
389
|
+
}
|
|
390
|
+
function shouldMergeTrackedAttributes(left, right) {
|
|
391
|
+
if (!left || !right) {
|
|
392
|
+
log.warn('passed undefined dataTracked attributes to shouldMergeTrackedAttributes', {
|
|
393
|
+
left,
|
|
394
|
+
right,
|
|
395
|
+
});
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
return (left.status === right.status &&
|
|
399
|
+
left.operation === right.operation &&
|
|
400
|
+
left.authorID === right.authorID);
|
|
401
|
+
}
|
|
402
|
+
function getMergeableMarkTrackedAttrs(node, attrs, schema) {
|
|
403
|
+
const nodeAttrs = getInlineNodeTrackedMarkData(node, schema);
|
|
404
|
+
return nodeAttrs && shouldMergeTrackedAttributes(nodeAttrs, attrs) ? nodeAttrs : null;
|
|
405
|
+
}
|
|
406
|
+
|
|
302
407
|
/*!
|
|
303
408
|
* © 2021 Atypon Systems LLC
|
|
304
409
|
*
|
|
@@ -327,15 +432,17 @@ function deleteNode(node, pos, tr) {
|
|
|
327
432
|
var _a;
|
|
328
433
|
const startPos = tr.doc.resolve(pos + 1);
|
|
329
434
|
const range = startPos.blockRange(tr.doc.resolve(startPos.pos - 2 + node.nodeSize));
|
|
330
|
-
const targetDepth = range && liftTarget(range);
|
|
331
|
-
// Check with typeof since with
|
|
435
|
+
const targetDepth = range && prosemirrorTransform.liftTarget(range);
|
|
436
|
+
// Check with typeof since with prosemirror-transform pre 1.6.0 targetDepth is undefined
|
|
332
437
|
if (range && typeof targetDepth === 'number') {
|
|
333
438
|
return tr.lift(range, targetDepth);
|
|
334
439
|
}
|
|
335
440
|
const resPos = tr.doc.resolve(pos);
|
|
336
|
-
|
|
441
|
+
// Block nodes can be deleted by just removing their start token which should then merge the text
|
|
442
|
+
// content to above node's content (if there is one)
|
|
443
|
+
const canMergeToNodeAbove = (resPos.parent !== tr.doc || resPos.nodeBefore) && node.isBlock && ((_a = node.firstChild) === null || _a === void 0 ? void 0 : _a.isText);
|
|
337
444
|
if (canMergeToNodeAbove) {
|
|
338
|
-
return tr.replaceWith(pos - 1, pos + 1, Fragment.empty);
|
|
445
|
+
return tr.replaceWith(pos - 1, pos + 1, prosemirrorModel.Fragment.empty);
|
|
339
446
|
}
|
|
340
447
|
else {
|
|
341
448
|
// NOTE: there's an edge case where moving content is not possible but because the immediate
|
|
@@ -368,98 +475,22 @@ function deleteNode(node, pos, tr) {
|
|
|
368
475
|
*/
|
|
369
476
|
function mergeNode(node, pos, tr) {
|
|
370
477
|
var _a;
|
|
371
|
-
if (canJoin(tr.doc, pos)) {
|
|
478
|
+
if (prosemirrorTransform.canJoin(tr.doc, pos)) {
|
|
372
479
|
return tr.join(pos);
|
|
373
480
|
}
|
|
374
|
-
else if (canJoin(tr.doc, pos + node.nodeSize)) {
|
|
481
|
+
else if (prosemirrorTransform.canJoin(tr.doc, pos + node.nodeSize)) {
|
|
482
|
+
// TODO should copy the attributes from the merged node below
|
|
375
483
|
return tr.join(pos + node.nodeSize);
|
|
376
484
|
}
|
|
377
|
-
// TODO is this the same thing as join?
|
|
485
|
+
// TODO is this the same thing as join to above?
|
|
378
486
|
const resPos = tr.doc.resolve(pos);
|
|
379
487
|
const canMergeToNodeAbove = (resPos.parent !== tr.doc || resPos.nodeBefore) && ((_a = node.firstChild) === null || _a === void 0 ? void 0 : _a.isText);
|
|
380
488
|
if (canMergeToNodeAbove) {
|
|
381
|
-
return tr.replaceWith(pos - 1, pos + 1, Fragment.empty);
|
|
489
|
+
return tr.replaceWith(pos - 1, pos + 1, prosemirrorModel.Fragment.empty);
|
|
382
490
|
}
|
|
383
491
|
return undefined;
|
|
384
492
|
}
|
|
385
493
|
|
|
386
|
-
/*!
|
|
387
|
-
* © 2021 Atypon Systems LLC
|
|
388
|
-
*
|
|
389
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
390
|
-
* you may not use this file except in compliance with the License.
|
|
391
|
-
* You may obtain a copy of the License at
|
|
392
|
-
*
|
|
393
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
394
|
-
*
|
|
395
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
396
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
397
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
398
|
-
* See the License for the specific language governing permissions and
|
|
399
|
-
* limitations under the License.
|
|
400
|
-
*/
|
|
401
|
-
function uuidv4() {
|
|
402
|
-
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
|
403
|
-
const r = (Math.random() * 16) | 0, v = c == 'x' ? r : (r & 0x3) | 0x8;
|
|
404
|
-
return v.toString(16);
|
|
405
|
-
});
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
function addTrackIdIfDoesntExist(attrs) {
|
|
409
|
-
if (!attrs.id) {
|
|
410
|
-
return {
|
|
411
|
-
id: uuidv4(),
|
|
412
|
-
...attrs,
|
|
413
|
-
};
|
|
414
|
-
}
|
|
415
|
-
return attrs;
|
|
416
|
-
}
|
|
417
|
-
function getInlineNodeTrackedMarkData(node, schema) {
|
|
418
|
-
if (!node || !node.isInline) {
|
|
419
|
-
return undefined;
|
|
420
|
-
}
|
|
421
|
-
const marksTrackedData = [];
|
|
422
|
-
node.marks.forEach((mark) => {
|
|
423
|
-
if (mark.type === schema.marks.tracked_insert || mark.type === schema.marks.tracked_delete) {
|
|
424
|
-
const operation = mark.type === schema.marks.tracked_insert
|
|
425
|
-
? CHANGE_OPERATION.insert
|
|
426
|
-
: CHANGE_OPERATION.delete;
|
|
427
|
-
marksTrackedData.push({ ...mark.attrs.dataTracked, operation });
|
|
428
|
-
}
|
|
429
|
-
});
|
|
430
|
-
if (marksTrackedData.length > 1) {
|
|
431
|
-
log.warn('inline node with more than 1 of tracked marks', marksTrackedData);
|
|
432
|
-
}
|
|
433
|
-
return marksTrackedData[0] || undefined;
|
|
434
|
-
}
|
|
435
|
-
function getNodeTrackedData(node, schema) {
|
|
436
|
-
return !node
|
|
437
|
-
? undefined
|
|
438
|
-
: node.isText
|
|
439
|
-
? getInlineNodeTrackedMarkData(node, schema)
|
|
440
|
-
: node.attrs.dataTracked;
|
|
441
|
-
}
|
|
442
|
-
function equalMarks(n1, n2) {
|
|
443
|
-
return (n1.marks.length === n2.marks.length &&
|
|
444
|
-
n1.marks.every((mark) => n1.marks.find((m) => m.type === mark.type)));
|
|
445
|
-
}
|
|
446
|
-
function shouldMergeTrackedAttributes(left, right) {
|
|
447
|
-
if (!left || !right) {
|
|
448
|
-
log.warn('passed undefined dataTracked attributes to shouldMergeTrackedAttributes', {
|
|
449
|
-
left,
|
|
450
|
-
right,
|
|
451
|
-
});
|
|
452
|
-
return false;
|
|
453
|
-
}
|
|
454
|
-
return (left.status === right.status &&
|
|
455
|
-
left.operation === right.operation &&
|
|
456
|
-
left.userID === right.userID);
|
|
457
|
-
}
|
|
458
|
-
function getMergeableMarkTrackedAttrs(node, attrs, schema) {
|
|
459
|
-
const nodeAttrs = getInlineNodeTrackedMarkData(node, schema);
|
|
460
|
-
return nodeAttrs && shouldMergeTrackedAttributes(nodeAttrs, attrs) ? nodeAttrs : null;
|
|
461
|
-
}
|
|
462
|
-
|
|
463
494
|
function updateChangeAttrs(tr, change, trackedAttrs, schema) {
|
|
464
495
|
const node = tr.doc.nodeAt(change.from);
|
|
465
496
|
if (!node) {
|
|
@@ -497,9 +528,9 @@ function updateChangeChildrenAttributes(changes, tr, mapping) {
|
|
|
497
528
|
* @param changes
|
|
498
529
|
* @param deleteMap
|
|
499
530
|
*/
|
|
500
|
-
function applyAcceptedRejectedChanges(tr, schema, changes, deleteMap = new Mapping()) {
|
|
531
|
+
function applyAcceptedRejectedChanges(tr, schema, changes, deleteMap = new prosemirrorTransform.Mapping()) {
|
|
501
532
|
changes.forEach((change) => {
|
|
502
|
-
if (change.attrs.status === CHANGE_STATUS.pending) {
|
|
533
|
+
if (change.attrs.status === exports.CHANGE_STATUS.pending) {
|
|
503
534
|
return;
|
|
504
535
|
}
|
|
505
536
|
// Map change.from and skip those which dont need to be applied
|
|
@@ -532,6 +563,16 @@ function applyAcceptedRejectedChanges(tr, schema, changes, deleteMap = new Mappi
|
|
|
532
563
|
}
|
|
533
564
|
deleteMap.appendMap(tr.steps[tr.steps.length - 1].getMap());
|
|
534
565
|
}
|
|
566
|
+
else if (ChangeSet.isNodeAttrChange(change) &&
|
|
567
|
+
change.attrs.status === exports.CHANGE_STATUS.accepted) {
|
|
568
|
+
const attrs = { ...node.attrs, dataTracked: null };
|
|
569
|
+
tr.setNodeMarkup(from, undefined, attrs, node.marks);
|
|
570
|
+
}
|
|
571
|
+
else if (ChangeSet.isNodeAttrChange(change) &&
|
|
572
|
+
change.attrs.status === exports.CHANGE_STATUS.rejected) {
|
|
573
|
+
const attrs = { ...change.oldAttrs, dataTracked: null };
|
|
574
|
+
tr.setNodeMarkup(from, undefined, attrs, node.marks);
|
|
575
|
+
}
|
|
535
576
|
});
|
|
536
577
|
return deleteMap;
|
|
537
578
|
}
|
|
@@ -573,6 +614,7 @@ function findChanges(state) {
|
|
|
573
614
|
type: 'text-change',
|
|
574
615
|
from: pos,
|
|
575
616
|
to: pos + node.nodeSize,
|
|
617
|
+
text: node.text,
|
|
576
618
|
attrs,
|
|
577
619
|
},
|
|
578
620
|
node,
|
|
@@ -617,13 +659,15 @@ function fixInconsistentChanges(changeSet, trackUserID, newTr, schema) {
|
|
|
617
659
|
const iteratedIds = new Set();
|
|
618
660
|
let changed = false;
|
|
619
661
|
changeSet.invalidChanges.forEach((c) => {
|
|
620
|
-
const { id,
|
|
662
|
+
const { id, authorID, operation, reviewedByID, status, createdAt, updatedAt } = c.attrs;
|
|
621
663
|
const newAttrs = {
|
|
622
664
|
...((!id || iteratedIds.has(id) || id.length === 0) && { id: uuidv4() }),
|
|
623
|
-
...(!
|
|
624
|
-
...(!operation && { operation: CHANGE_OPERATION.insert }),
|
|
625
|
-
...(!
|
|
665
|
+
...(!authorID && { authorID: trackUserID }),
|
|
666
|
+
...(!operation && { operation: exports.CHANGE_OPERATION.insert }),
|
|
667
|
+
...(!reviewedByID && { reviewedByID: null }),
|
|
668
|
+
...(!status && { status: exports.CHANGE_STATUS.pending }),
|
|
626
669
|
...(!createdAt && { createdAt: Date.now() }),
|
|
670
|
+
...(!updatedAt && { updatedAt: Date.now() }),
|
|
627
671
|
};
|
|
628
672
|
if (Object.keys(newAttrs).length > 0) {
|
|
629
673
|
updateChangeAttrs(newTr, c, { ...c.attrs, ...newAttrs }, schema);
|
|
@@ -634,87 +678,6 @@ function fixInconsistentChanges(changeSet, trackUserID, newTr, schema) {
|
|
|
634
678
|
return changed;
|
|
635
679
|
}
|
|
636
680
|
|
|
637
|
-
/*!
|
|
638
|
-
* © 2021 Atypon Systems LLC
|
|
639
|
-
*
|
|
640
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
641
|
-
* you may not use this file except in compliance with the License.
|
|
642
|
-
* You may obtain a copy of the License at
|
|
643
|
-
*
|
|
644
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
645
|
-
*
|
|
646
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
647
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
648
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
649
|
-
* See the License for the specific language governing permissions and
|
|
650
|
-
* limitations under the License.
|
|
651
|
-
*/
|
|
652
|
-
function markInlineNodeChange(node, newTrackAttrs, schema) {
|
|
653
|
-
const filtered = node.marks.filter((m) => m.type !== schema.marks.tracked_insert && m.type !== schema.marks.tracked_delete);
|
|
654
|
-
const mark = newTrackAttrs.operation === CHANGE_OPERATION.insert
|
|
655
|
-
? schema.marks.tracked_insert
|
|
656
|
-
: schema.marks.tracked_delete;
|
|
657
|
-
const createdMark = mark.create({
|
|
658
|
-
dataTracked: addTrackIdIfDoesntExist(newTrackAttrs),
|
|
659
|
-
});
|
|
660
|
-
return node.mark(filtered.concat(createdMark));
|
|
661
|
-
}
|
|
662
|
-
function recurseNodeContent(node, newTrackAttrs, schema) {
|
|
663
|
-
if (node.isText) {
|
|
664
|
-
return markInlineNodeChange(node, newTrackAttrs, schema);
|
|
665
|
-
}
|
|
666
|
-
else if (node.isBlock || node.isInline) {
|
|
667
|
-
const updatedChildren = [];
|
|
668
|
-
node.content.forEach((child) => {
|
|
669
|
-
updatedChildren.push(recurseNodeContent(child, newTrackAttrs, schema));
|
|
670
|
-
});
|
|
671
|
-
return node.type.create({
|
|
672
|
-
...node.attrs,
|
|
673
|
-
dataTracked: addTrackIdIfDoesntExist(newTrackAttrs),
|
|
674
|
-
}, Fragment.fromArray(updatedChildren), node.marks);
|
|
675
|
-
}
|
|
676
|
-
else {
|
|
677
|
-
log.error(`unhandled node type: "${node.type.name}"`, node);
|
|
678
|
-
return node;
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
function setFragmentAsInserted(inserted, insertAttrs, schema) {
|
|
682
|
-
// Recurse the content in the inserted slice and either mark it tracked_insert or set node attrs
|
|
683
|
-
const updatedInserted = [];
|
|
684
|
-
inserted.forEach((n) => {
|
|
685
|
-
updatedInserted.push(recurseNodeContent(n, insertAttrs, schema));
|
|
686
|
-
});
|
|
687
|
-
return updatedInserted.length === 0 ? inserted : Fragment.fromArray(updatedInserted);
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
/*!
|
|
691
|
-
* © 2021 Atypon Systems LLC
|
|
692
|
-
*
|
|
693
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
694
|
-
* you may not use this file except in compliance with the License.
|
|
695
|
-
* You may obtain a copy of the License at
|
|
696
|
-
*
|
|
697
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
698
|
-
*
|
|
699
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
700
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
701
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
702
|
-
* See the License for the specific language governing permissions and
|
|
703
|
-
* limitations under the License.
|
|
704
|
-
*/
|
|
705
|
-
function createNewInsertAttrs(attrs) {
|
|
706
|
-
return {
|
|
707
|
-
...attrs,
|
|
708
|
-
operation: CHANGE_OPERATION.insert,
|
|
709
|
-
};
|
|
710
|
-
}
|
|
711
|
-
function createNewDeleteAttrs(attrs) {
|
|
712
|
-
return {
|
|
713
|
-
...attrs,
|
|
714
|
-
operation: CHANGE_OPERATION.delete,
|
|
715
|
-
};
|
|
716
|
-
}
|
|
717
|
-
|
|
718
681
|
/*!
|
|
719
682
|
* © 2021 Atypon Systems LLC
|
|
720
683
|
*
|
|
@@ -754,7 +717,7 @@ function getMergedNode(node, currentDepth, depth, first) {
|
|
|
754
717
|
};
|
|
755
718
|
}
|
|
756
719
|
const result = [];
|
|
757
|
-
let merged = Fragment.empty;
|
|
720
|
+
let merged = prosemirrorModel.Fragment.empty;
|
|
758
721
|
node.content.forEach((n, _, i) => {
|
|
759
722
|
if ((first && i === 0) || (!first && i === node.childCount - 1)) {
|
|
760
723
|
const { mergedNodeContent, unmergedContent } = getMergedNode(n, currentDepth + 1, depth, first);
|
|
@@ -769,7 +732,7 @@ function getMergedNode(node, currentDepth, depth, first) {
|
|
|
769
732
|
});
|
|
770
733
|
return {
|
|
771
734
|
mergedNodeContent: merged,
|
|
772
|
-
unmergedContent: result.length > 0 ? Fragment.fromArray(result) : undefined,
|
|
735
|
+
unmergedContent: result.length > 0 ? prosemirrorModel.Fragment.fromArray(result) : undefined,
|
|
773
736
|
};
|
|
774
737
|
}
|
|
775
738
|
/**
|
|
@@ -810,70 +773,104 @@ function splitSliceIntoMergedParts(insertSlice, mergeEqualSides = false) {
|
|
|
810
773
|
firstMergedNode,
|
|
811
774
|
lastMergedNode,
|
|
812
775
|
};
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/*!
|
|
779
|
+
* © 2021 Atypon Systems LLC
|
|
816
780
|
*
|
|
817
|
-
*
|
|
818
|
-
*
|
|
819
|
-
*
|
|
820
|
-
*
|
|
821
|
-
*
|
|
822
|
-
*
|
|
823
|
-
*
|
|
824
|
-
*
|
|
825
|
-
*
|
|
781
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
782
|
+
* you may not use this file except in compliance with the License.
|
|
783
|
+
* You may obtain a copy of the License at
|
|
784
|
+
*
|
|
785
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
786
|
+
*
|
|
787
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
788
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
789
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
790
|
+
* See the License for the specific language governing permissions and
|
|
791
|
+
* limitations under the License.
|
|
826
792
|
*/
|
|
827
|
-
function
|
|
828
|
-
const
|
|
829
|
-
const
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
793
|
+
function markInlineNodeChange(node, newTrackAttrs, schema) {
|
|
794
|
+
const filtered = node.marks.filter((m) => m.type !== schema.marks.tracked_insert && m.type !== schema.marks.tracked_delete);
|
|
795
|
+
const mark = newTrackAttrs.operation === exports.CHANGE_OPERATION.insert
|
|
796
|
+
? schema.marks.tracked_insert
|
|
797
|
+
: schema.marks.tracked_delete;
|
|
798
|
+
const createdMark = mark.create({
|
|
799
|
+
dataTracked: addTrackIdIfDoesntExist(newTrackAttrs),
|
|
800
|
+
});
|
|
801
|
+
return node.mark(filtered.concat(createdMark));
|
|
802
|
+
}
|
|
803
|
+
function recurseNodeContent(node, newTrackAttrs, schema) {
|
|
804
|
+
if (node.isText) {
|
|
805
|
+
return markInlineNodeChange(node, newTrackAttrs, schema);
|
|
837
806
|
}
|
|
838
|
-
else {
|
|
839
|
-
const
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
const rightMarks = getMergeableMarkTrackedAttrs(rightNode, deleteAttrs, schema);
|
|
843
|
-
const fromStartOfMark = start - (leftNode && leftMarks ? leftNode.nodeSize : 0);
|
|
844
|
-
const toEndOfMark = end + (rightNode && rightMarks ? rightNode.nodeSize : 0);
|
|
845
|
-
const dataTracked = addTrackIdIfDoesntExist({
|
|
846
|
-
...leftMarks,
|
|
847
|
-
...rightMarks,
|
|
848
|
-
...deleteAttrs,
|
|
807
|
+
else if (node.isBlock || node.isInline) {
|
|
808
|
+
const updatedChildren = [];
|
|
809
|
+
node.content.forEach((child) => {
|
|
810
|
+
updatedChildren.push(recurseNodeContent(child, newTrackAttrs, schema));
|
|
849
811
|
});
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
|
-
/**
|
|
857
|
-
* Deletes inserted block or inline node, otherwise adds `dataTracked` object with CHANGE_STATUS 'deleted'
|
|
858
|
-
* @param node
|
|
859
|
-
* @param pos
|
|
860
|
-
* @param newTr
|
|
861
|
-
* @param deleteAttrs
|
|
862
|
-
*/
|
|
863
|
-
function deleteOrSetNodeDeleted(node, pos, newTr, deleteAttrs) {
|
|
864
|
-
const dataTracked = node.attrs.dataTracked;
|
|
865
|
-
const wasInsertedBySameUser = (dataTracked === null || dataTracked === void 0 ? void 0 : dataTracked.operation) === CHANGE_OPERATION.insert && dataTracked.userID === deleteAttrs.userID;
|
|
866
|
-
if (wasInsertedBySameUser) {
|
|
867
|
-
deleteNode(node, pos, newTr);
|
|
812
|
+
return node.type.create({
|
|
813
|
+
...node.attrs,
|
|
814
|
+
dataTracked: addTrackIdIfDoesntExist(newTrackAttrs),
|
|
815
|
+
}, prosemirrorModel.Fragment.fromArray(updatedChildren), node.marks);
|
|
868
816
|
}
|
|
869
817
|
else {
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
dataTracked: addTrackIdIfDoesntExist(deleteAttrs),
|
|
873
|
-
};
|
|
874
|
-
newTr.setNodeMarkup(pos, undefined, attrs, node.marks);
|
|
818
|
+
log.error(`unhandled node type: "${node.type.name}"`, node);
|
|
819
|
+
return node;
|
|
875
820
|
}
|
|
876
821
|
}
|
|
822
|
+
function setFragmentAsInserted(inserted, insertAttrs, schema) {
|
|
823
|
+
// Recurse the content in the inserted slice and either mark it tracked_insert or set node attrs
|
|
824
|
+
const updatedInserted = [];
|
|
825
|
+
inserted.forEach((n) => {
|
|
826
|
+
updatedInserted.push(recurseNodeContent(n, insertAttrs, schema));
|
|
827
|
+
});
|
|
828
|
+
return updatedInserted.length === 0 ? inserted : prosemirrorModel.Fragment.fromArray(updatedInserted);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/*!
|
|
832
|
+
* © 2021 Atypon Systems LLC
|
|
833
|
+
*
|
|
834
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
835
|
+
* you may not use this file except in compliance with the License.
|
|
836
|
+
* You may obtain a copy of the License at
|
|
837
|
+
*
|
|
838
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
839
|
+
*
|
|
840
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
841
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
842
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
843
|
+
* See the License for the specific language governing permissions and
|
|
844
|
+
* limitations under the License.
|
|
845
|
+
*/
|
|
846
|
+
function createNewInsertAttrs(attrs) {
|
|
847
|
+
return {
|
|
848
|
+
...attrs,
|
|
849
|
+
operation: exports.CHANGE_OPERATION.insert,
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
function createNewDeleteAttrs(attrs) {
|
|
853
|
+
return {
|
|
854
|
+
...attrs,
|
|
855
|
+
operation: exports.CHANGE_OPERATION.delete,
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
/*!
|
|
860
|
+
* © 2021 Atypon Systems LLC
|
|
861
|
+
*
|
|
862
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
863
|
+
* you may not use this file except in compliance with the License.
|
|
864
|
+
* You may obtain a copy of the License at
|
|
865
|
+
*
|
|
866
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
867
|
+
*
|
|
868
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
869
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
870
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
871
|
+
* See the License for the specific language governing permissions and
|
|
872
|
+
* limitations under the License.
|
|
873
|
+
*/
|
|
877
874
|
/**
|
|
878
875
|
* Applies deletion to the doc without actually deleting nodes that have not been inserted
|
|
879
876
|
*
|
|
@@ -900,28 +897,39 @@ function deleteOrSetNodeDeleted(node, pos, newTr, deleteAttrs) {
|
|
|
900
897
|
* @returns mapping adjusted by the applied operations & modified insert slice
|
|
901
898
|
*/
|
|
902
899
|
function deleteAndMergeSplitNodes(from, to, gap, startDoc, newTr, schema, trackAttrs, insertSlice) {
|
|
903
|
-
const deleteMap = new Mapping();
|
|
904
|
-
const
|
|
900
|
+
const deleteMap = new prosemirrorTransform.Mapping();
|
|
901
|
+
const steps = [];
|
|
905
902
|
// No deletion applied, return default values
|
|
906
903
|
if (from === to) {
|
|
907
904
|
return {
|
|
908
905
|
deleteMap,
|
|
909
|
-
mergedInsertPos,
|
|
910
906
|
newSliceContent: insertSlice.content,
|
|
907
|
+
steps,
|
|
911
908
|
};
|
|
912
909
|
}
|
|
913
910
|
const { openStart, openEnd } = insertSlice;
|
|
914
911
|
const { updatedSliceNodes, firstMergedNode, lastMergedNode } = splitSliceIntoMergedParts(insertSlice, gap !== undefined);
|
|
915
|
-
const deleteAttrs = createNewDeleteAttrs(trackAttrs);
|
|
916
912
|
let mergingStartSide = true;
|
|
917
913
|
startDoc.nodesBetween(from, to, (node, pos) => {
|
|
918
914
|
const { pos: offsetPos, deleted: nodeWasDeleted } = deleteMap.mapResult(pos, 1);
|
|
919
915
|
const offsetFrom = deleteMap.map(from, -1);
|
|
920
916
|
const offsetTo = deleteMap.map(to, 1);
|
|
921
|
-
const wasWithinGap = gap && offsetPos >= deleteMap.map(gap.start, -1);
|
|
922
917
|
const nodeEnd = offsetPos + node.nodeSize;
|
|
918
|
+
// So this insane boolean checks for ReplaceAroundStep gaps and whether the node should be skipped
|
|
919
|
+
// since the content inside gap should stay unchanged.
|
|
920
|
+
// All other nodes except text nodes consist of one start and end token (or just a single token for atoms).
|
|
921
|
+
// For them we can just check whether the start token is within the gap eg pos is 10 when gap (8, 18) to
|
|
922
|
+
// determine whether it should be skipped.
|
|
923
|
+
// For text nodes though, since they are continous, they might only partially be enclosed in the gap
|
|
924
|
+
// eg. pos 10 when gap is (8, 18) BUT if their nodeEnd goes past the gap's end eg nodeEnd 20 they actually
|
|
925
|
+
// are altered and should not be skipped.
|
|
926
|
+
// @TODO ATM 20.7.2022 there doesn't seem to be tests that capture this.
|
|
927
|
+
const wasWithinGap = gap &&
|
|
928
|
+
((!node.isText && offsetPos >= deleteMap.map(gap.start, -1)) ||
|
|
929
|
+
(node.isText &&
|
|
930
|
+
offsetPos <= deleteMap.map(gap.start, -1) &&
|
|
931
|
+
nodeEnd >= deleteMap.map(gap.end, -1)));
|
|
923
932
|
let step = newTr.steps[newTr.steps.length - 1];
|
|
924
|
-
// debugger
|
|
925
933
|
// nodeEnd > offsetFrom -> delete touches this node
|
|
926
934
|
// eg (del 6 10) <p 5>|<t 6>cdf</t 9></p 10>| -> <p> nodeEnd 10 > from 6
|
|
927
935
|
//
|
|
@@ -976,24 +984,18 @@ function deleteAndMergeSplitNodes(from, to, gap, startDoc, newTr, schema, trackA
|
|
|
976
984
|
// ProseMirror node semantics as start tokens are considered to contain the actual node itself.
|
|
977
985
|
const mergeEndNode = startTokenDeleted && openEnd > 0 && depth === openEnd && mergeContent !== undefined;
|
|
978
986
|
if (mergeStartNode || mergeEndNode) {
|
|
979
|
-
// The default insert position for block nodes is either the start of the merged content or the end.
|
|
980
|
-
// Incase text was merged, this must be updated as the start or end of the node doesn't map to the
|
|
981
|
-
// actual position of the merge. Currently the inserted content is inserted at the start or end
|
|
982
|
-
// of the merged content, TODO reverse the start/end when end/start token?
|
|
983
|
-
let insertPos = mergeStartNode ? nodeEnd - openStart : offsetPos + openEnd;
|
|
984
|
-
if (node.isText) {
|
|
985
|
-
// When merging text we must delete text in the same go as well, as the from/to boundary goes through
|
|
986
|
-
// the text node.
|
|
987
|
-
insertPos = deleteTextIfInserted(node, offsetPos, newTr, schema, deleteAttrs, offsetFrom, offsetTo);
|
|
988
|
-
deleteMap.appendMap(newTr.steps[newTr.steps.length - 1].getMap());
|
|
989
|
-
step = newTr.steps[newTr.steps.length - 1];
|
|
990
|
-
}
|
|
991
987
|
// Just as a fun fact that I found out while debugging this. Inserting text at paragraph position wraps
|
|
992
988
|
// it into a new paragraph(!). So that's why you always offset your positions to insert it _inside_
|
|
993
989
|
// the paragraph.
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
990
|
+
steps.push({
|
|
991
|
+
type: 'merge-fragment',
|
|
992
|
+
pos: offsetPos,
|
|
993
|
+
mergePos: mergeStartNode ? nodeEnd - openStart : offsetPos + openEnd,
|
|
994
|
+
from: offsetFrom,
|
|
995
|
+
to: offsetTo,
|
|
996
|
+
node,
|
|
997
|
+
fragment: setFragmentAsInserted(mergeContent, createNewInsertAttrs(trackAttrs), schema),
|
|
998
|
+
});
|
|
997
999
|
// Okay this is a bit ridiculous but it's used to adjust the insert pos when track changes prevents deletions
|
|
998
1000
|
// of merged nodes & content, as just using mapped toA in that case isn't the same.
|
|
999
1001
|
// The calculation is a bit mysterious, I admit.
|
|
@@ -1006,12 +1008,23 @@ function deleteAndMergeSplitNodes(from, to, gap, startDoc, newTr, schema, trackA
|
|
|
1006
1008
|
else if (node.isText) {
|
|
1007
1009
|
// Text deletion is handled even when the deletion doesn't completely wrap the text node
|
|
1008
1010
|
// (which is basically the case most of the time)
|
|
1009
|
-
|
|
1011
|
+
steps.push({
|
|
1012
|
+
type: 'delete-text',
|
|
1013
|
+
pos: offsetPos,
|
|
1014
|
+
from: Math.max(offsetPos, offsetFrom),
|
|
1015
|
+
to: Math.min(nodeEnd, offsetTo),
|
|
1016
|
+
node,
|
|
1017
|
+
});
|
|
1010
1018
|
}
|
|
1011
1019
|
else ;
|
|
1012
1020
|
}
|
|
1013
1021
|
else if (nodeCompletelyDeleted) {
|
|
1014
|
-
|
|
1022
|
+
steps.push({
|
|
1023
|
+
type: 'delete-node',
|
|
1024
|
+
pos: offsetPos,
|
|
1025
|
+
nodeEnd: nodeEnd,
|
|
1026
|
+
node,
|
|
1027
|
+
});
|
|
1015
1028
|
}
|
|
1016
1029
|
}
|
|
1017
1030
|
const newestStep = newTr.steps[newTr.steps.length - 1];
|
|
@@ -1021,10 +1034,10 @@ function deleteAndMergeSplitNodes(from, to, gap, startDoc, newTr, schema, trackA
|
|
|
1021
1034
|
});
|
|
1022
1035
|
return {
|
|
1023
1036
|
deleteMap,
|
|
1024
|
-
mergedInsertPos,
|
|
1025
1037
|
newSliceContent: updatedSliceNodes
|
|
1026
|
-
? Fragment.fromArray(updatedSliceNodes)
|
|
1038
|
+
? prosemirrorModel.Fragment.fromArray(updatedSliceNodes)
|
|
1027
1039
|
: insertSlice.content,
|
|
1040
|
+
steps,
|
|
1028
1041
|
};
|
|
1029
1042
|
}
|
|
1030
1043
|
|
|
@@ -1047,18 +1060,20 @@ function mergeTrackedMarks(pos, doc, newTr, schema) {
|
|
|
1047
1060
|
if (!nodeAfter || !nodeBefore || !leftMark || !rightMark || leftMark.type !== rightMark.type) {
|
|
1048
1061
|
return;
|
|
1049
1062
|
}
|
|
1050
|
-
const
|
|
1051
|
-
const
|
|
1052
|
-
if (!shouldMergeTrackedAttributes(
|
|
1063
|
+
const leftDataTracked = leftMark.attrs.dataTracked;
|
|
1064
|
+
const rightDataTracked = rightMark.attrs.dataTracked;
|
|
1065
|
+
if (!shouldMergeTrackedAttributes(leftDataTracked, rightDataTracked)) {
|
|
1053
1066
|
return;
|
|
1054
1067
|
}
|
|
1055
|
-
const
|
|
1056
|
-
|
|
1057
|
-
|
|
1068
|
+
const isLeftOlder = (leftDataTracked.createdAt || 0) < (rightDataTracked.createdAt || 0);
|
|
1069
|
+
const ancestorAttrs = isLeftOlder ? leftDataTracked : rightDataTracked;
|
|
1070
|
+
const dataTracked = {
|
|
1071
|
+
...ancestorAttrs,
|
|
1072
|
+
updatedAt: Date.now(),
|
|
1058
1073
|
};
|
|
1059
1074
|
const fromStartOfMark = pos - nodeBefore.nodeSize;
|
|
1060
1075
|
const toEndOfMark = pos + nodeAfter.nodeSize;
|
|
1061
|
-
newTr.addMark(fromStartOfMark, toEndOfMark, leftMark.type.create(
|
|
1076
|
+
newTr.addMark(fromStartOfMark, toEndOfMark, leftMark.type.create({ ...leftMark.attrs, dataTracked }));
|
|
1062
1077
|
}
|
|
1063
1078
|
|
|
1064
1079
|
/*!
|
|
@@ -1106,14 +1121,16 @@ function trackReplaceAroundStep(step, oldState, newTr, attrs) {
|
|
|
1106
1121
|
const stepResult = newTr.maybeStep(newStep);
|
|
1107
1122
|
if (stepResult.failed) {
|
|
1108
1123
|
log.error(`inverting ReplaceAroundStep failed: "${stepResult.failed}"`, newStep);
|
|
1109
|
-
return;
|
|
1124
|
+
return [];
|
|
1110
1125
|
}
|
|
1111
1126
|
const gap = oldState.doc.slice(gapFrom, gapTo);
|
|
1112
1127
|
log.info('RETAINED GAP CONTENT', gap);
|
|
1113
1128
|
// First apply the deleted range and update the insert slice to not include content that was deleted,
|
|
1114
1129
|
// eg partial nodes in an open-ended slice
|
|
1115
|
-
const { deleteMap, newSliceContent } = deleteAndMergeSplitNodes(from, to, { start: gapFrom, end: gapTo }, newTr.doc, newTr, oldState.schema, attrs, slice);
|
|
1130
|
+
const { deleteMap, newSliceContent, steps: deleteSteps, } = deleteAndMergeSplitNodes(from, to, { start: gapFrom, end: gapTo }, newTr.doc, newTr, oldState.schema, attrs, slice);
|
|
1131
|
+
let steps = deleteSteps;
|
|
1116
1132
|
log.info('TR: new steps after applying delete', [...newTr.steps]);
|
|
1133
|
+
log.info('DELETE STEPS: ', deleteSteps);
|
|
1117
1134
|
// We only want to insert when there something inside the gap (actually would this be always true?)
|
|
1118
1135
|
// or insert slice wasn't just start/end tokens (which we already merged inside deleteAndMergeSplitBlockNodes)
|
|
1119
1136
|
if (gap.size > 0 || (!structure && newSliceContent.size > 0)) {
|
|
@@ -1122,27 +1139,25 @@ function trackReplaceAroundStep(step, oldState, newTr, attrs) {
|
|
|
1122
1139
|
// the sides should be equal. TODO can they be other than 0?
|
|
1123
1140
|
const openStart = slice.openStart !== slice.openEnd || newSliceContent.size === 0 ? 0 : slice.openStart;
|
|
1124
1141
|
const openEnd = slice.openStart !== slice.openEnd || newSliceContent.size === 0 ? 0 : slice.openEnd;
|
|
1125
|
-
let insertedSlice = new Slice(setFragmentAsInserted(newSliceContent, createNewInsertAttrs(attrs), oldState.schema), openStart, openEnd);
|
|
1142
|
+
let insertedSlice = new prosemirrorModel.Slice(setFragmentAsInserted(newSliceContent, createNewInsertAttrs(attrs), oldState.schema), openStart, openEnd);
|
|
1126
1143
|
if (gap.size > 0) {
|
|
1127
1144
|
log.info('insertedSlice before inserted gap', insertedSlice);
|
|
1128
1145
|
insertedSlice = insertedSlice.insertAt(insertedSlice.size === 0 ? 0 : insert, gap.content);
|
|
1129
1146
|
log.info('insertedSlice after inserted gap', insertedSlice);
|
|
1130
1147
|
}
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
}
|
|
1137
|
-
log.info('new steps after applying insert', [...newTr.steps]);
|
|
1138
|
-
mergeTrackedMarks(deleteMap.map(gapFrom), newTr.doc, newTr, oldState.schema);
|
|
1139
|
-
mergeTrackedMarks(deleteMap.map(gapTo), newTr.doc, newTr, oldState.schema);
|
|
1148
|
+
deleteSteps.push({
|
|
1149
|
+
type: 'insert-slice',
|
|
1150
|
+
from: deleteMap.map(gapFrom),
|
|
1151
|
+
to: deleteMap.map(gapTo),
|
|
1152
|
+
slice: insertedSlice,
|
|
1153
|
+
});
|
|
1140
1154
|
}
|
|
1141
1155
|
else {
|
|
1142
1156
|
// Incase only deletion was applied, check whether tracked marks around deleted content can be merged
|
|
1143
1157
|
mergeTrackedMarks(deleteMap.map(gapFrom), newTr.doc, newTr, oldState.schema);
|
|
1144
1158
|
mergeTrackedMarks(deleteMap.map(gapTo), newTr.doc, newTr, oldState.schema);
|
|
1145
1159
|
}
|
|
1160
|
+
return steps;
|
|
1146
1161
|
}
|
|
1147
1162
|
|
|
1148
1163
|
/*!
|
|
@@ -1162,7 +1177,7 @@ function trackReplaceAroundStep(step, oldState, newTr, attrs) {
|
|
|
1162
1177
|
*/
|
|
1163
1178
|
function trackReplaceStep(step, oldState, newTr, attrs) {
|
|
1164
1179
|
log.info('###### ReplaceStep ######');
|
|
1165
|
-
let selectionPos = 0;
|
|
1180
|
+
let selectionPos = 0, changeSteps = [];
|
|
1166
1181
|
step.getMap().forEach((fromA, toA, fromB, toB) => {
|
|
1167
1182
|
log.info(`changed ranges: ${fromA} ${toA} ${fromB} ${toB}`);
|
|
1168
1183
|
const { slice } = step;
|
|
@@ -1173,36 +1188,309 @@ function trackReplaceStep(step, oldState, newTr, attrs) {
|
|
|
1173
1188
|
log.error(`invert ReplaceStep failed: "${stepResult.failed}"`, newStep);
|
|
1174
1189
|
return;
|
|
1175
1190
|
}
|
|
1191
|
+
log.info('TR: steps before applying delete', [...newTr.steps]);
|
|
1176
1192
|
// First apply the deleted range and update the insert slice to not include content that was deleted,
|
|
1177
1193
|
// eg partial nodes in an open-ended slice
|
|
1178
|
-
const { deleteMap,
|
|
1179
|
-
|
|
1180
|
-
|
|
1194
|
+
const { deleteMap, newSliceContent, steps: deleteSteps, } = deleteAndMergeSplitNodes(fromA, toA, undefined, oldState.doc, newTr, oldState.schema, attrs, slice);
|
|
1195
|
+
changeSteps.push(...deleteSteps);
|
|
1196
|
+
log.info('TR: steps after applying delete', [...newTr.steps]);
|
|
1197
|
+
log.info('DELETE STEPS: ', changeSteps);
|
|
1198
|
+
const adjustedInsertPos = deleteMap.map(toA);
|
|
1181
1199
|
if (newSliceContent.size > 0) {
|
|
1182
1200
|
log.info('newSliceContent', newSliceContent);
|
|
1183
1201
|
// Since deleteAndMergeSplitBlockNodes modified the slice to not to contain any merged nodes,
|
|
1184
1202
|
// the sides should be equal. TODO can they be other than 0?
|
|
1185
1203
|
const openStart = slice.openStart !== slice.openEnd ? 0 : slice.openStart;
|
|
1186
1204
|
const openEnd = slice.openStart !== slice.openEnd ? 0 : slice.openEnd;
|
|
1187
|
-
|
|
1188
|
-
|
|
1205
|
+
changeSteps.push({
|
|
1206
|
+
type: 'insert-slice',
|
|
1207
|
+
from: adjustedInsertPos,
|
|
1208
|
+
to: adjustedInsertPos,
|
|
1209
|
+
slice: new prosemirrorModel.Slice(setFragmentAsInserted(newSliceContent, createNewInsertAttrs(attrs), oldState.schema), openStart, openEnd),
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
else {
|
|
1213
|
+
// Incase only deletion was applied, check whether tracked marks around deleted content can be merged
|
|
1214
|
+
mergeTrackedMarks(adjustedInsertPos, newTr.doc, newTr, oldState.schema);
|
|
1215
|
+
selectionPos = fromA;
|
|
1216
|
+
}
|
|
1217
|
+
});
|
|
1218
|
+
return [changeSteps, selectionPos];
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
/*!
|
|
1222
|
+
* © 2021 Atypon Systems LLC
|
|
1223
|
+
*
|
|
1224
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
1225
|
+
* you may not use this file except in compliance with the License.
|
|
1226
|
+
* You may obtain a copy of the License at
|
|
1227
|
+
*
|
|
1228
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
1229
|
+
*
|
|
1230
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
1231
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
1232
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
1233
|
+
* See the License for the specific language governing permissions and
|
|
1234
|
+
* limitations under the License.
|
|
1235
|
+
*/
|
|
1236
|
+
/**
|
|
1237
|
+
* Deletes inserted text directly, otherwise wraps it with tracked_delete mark
|
|
1238
|
+
*
|
|
1239
|
+
* This would work for general inline nodes too, but since node marks don't work properly
|
|
1240
|
+
* with Yjs, attributes are used instead.
|
|
1241
|
+
* @param node
|
|
1242
|
+
* @param pos
|
|
1243
|
+
* @param newTr
|
|
1244
|
+
* @param schema
|
|
1245
|
+
* @param deleteAttrs
|
|
1246
|
+
* @param from
|
|
1247
|
+
* @param to
|
|
1248
|
+
*/
|
|
1249
|
+
function deleteTextIfInserted(node, pos, newTr, schema, deleteAttrs, from, to) {
|
|
1250
|
+
const start = from ? Math.max(pos, from) : pos;
|
|
1251
|
+
const nodeEnd = pos + node.nodeSize;
|
|
1252
|
+
const end = to ? Math.min(nodeEnd, to) : nodeEnd;
|
|
1253
|
+
if (node.marks.find((m) => m.type === schema.marks.tracked_insert)) {
|
|
1254
|
+
// Math.max(pos, from) is for picking always the start of the node,
|
|
1255
|
+
// not the start of the change (which might span multiple nodes).
|
|
1256
|
+
// Pos can be less than from as nodesBetween iterates through all nodes starting from the top block node
|
|
1257
|
+
newTr.replaceWith(start, end, prosemirrorModel.Fragment.empty);
|
|
1258
|
+
return start;
|
|
1259
|
+
}
|
|
1260
|
+
else {
|
|
1261
|
+
const leftNode = newTr.doc.resolve(start).nodeBefore;
|
|
1262
|
+
const leftMarks = getMergeableMarkTrackedAttrs(leftNode, deleteAttrs, schema);
|
|
1263
|
+
const rightNode = newTr.doc.resolve(end).nodeAfter;
|
|
1264
|
+
const rightMarks = getMergeableMarkTrackedAttrs(rightNode, deleteAttrs, schema);
|
|
1265
|
+
const fromStartOfMark = start - (leftNode && leftMarks ? leftNode.nodeSize : 0);
|
|
1266
|
+
const toEndOfMark = end + (rightNode && rightMarks ? rightNode.nodeSize : 0);
|
|
1267
|
+
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);
|
|
1268
|
+
const dataTracked = addTrackIdIfDoesntExist({
|
|
1269
|
+
...leftMarks,
|
|
1270
|
+
...rightMarks,
|
|
1271
|
+
...deleteAttrs,
|
|
1272
|
+
createdAt,
|
|
1273
|
+
});
|
|
1274
|
+
newTr.addMark(fromStartOfMark, toEndOfMark, schema.marks.tracked_delete.create({
|
|
1275
|
+
dataTracked,
|
|
1276
|
+
}));
|
|
1277
|
+
return toEndOfMark;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
/*!
|
|
1282
|
+
* © 2021 Atypon Systems LLC
|
|
1283
|
+
*
|
|
1284
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
1285
|
+
* you may not use this file except in compliance with the License.
|
|
1286
|
+
* You may obtain a copy of the License at
|
|
1287
|
+
*
|
|
1288
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
1289
|
+
*
|
|
1290
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
1291
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
1292
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
1293
|
+
* See the License for the specific language governing permissions and
|
|
1294
|
+
* limitations under the License.
|
|
1295
|
+
*/
|
|
1296
|
+
function processChangeSteps(changes, startPos, newTr, emptyAttrs, schema) {
|
|
1297
|
+
const mapping = new prosemirrorTransform.Mapping();
|
|
1298
|
+
const deleteAttrs = createNewDeleteAttrs(emptyAttrs);
|
|
1299
|
+
let selectionPos = startPos;
|
|
1300
|
+
// @TODO add custom handler / condition?
|
|
1301
|
+
changes.forEach((c) => {
|
|
1302
|
+
let step = newTr.steps[newTr.steps.length - 1];
|
|
1303
|
+
log.info('process change: ', c);
|
|
1304
|
+
// const handled = customStepHandler(changes, newTr, emptyAttrs) // ChangeStep[] | undefined
|
|
1305
|
+
if (c.type === 'delete-node') {
|
|
1306
|
+
const dataTracked = c.node.attrs.dataTracked;
|
|
1307
|
+
const wasInsertedBySameUser = (dataTracked === null || dataTracked === void 0 ? void 0 : dataTracked.operation) === exports.CHANGE_OPERATION.insert &&
|
|
1308
|
+
dataTracked.authorID === emptyAttrs.authorID;
|
|
1309
|
+
if (wasInsertedBySameUser) {
|
|
1310
|
+
deleteNode(c.node, mapping.map(c.pos), newTr);
|
|
1311
|
+
const newestStep = newTr.steps[newTr.steps.length - 1];
|
|
1312
|
+
if (step !== newestStep) {
|
|
1313
|
+
mapping.appendMap(newestStep.getMap());
|
|
1314
|
+
step = newestStep;
|
|
1315
|
+
}
|
|
1316
|
+
mergeTrackedMarks(mapping.map(c.pos), newTr.doc, newTr, schema);
|
|
1317
|
+
}
|
|
1318
|
+
else {
|
|
1319
|
+
const attrs = {
|
|
1320
|
+
...c.node.attrs,
|
|
1321
|
+
dataTracked: addTrackIdIfDoesntExist(deleteAttrs),
|
|
1322
|
+
};
|
|
1323
|
+
newTr.setNodeMarkup(mapping.map(c.pos), undefined, attrs, c.node.marks);
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
else if (c.type === 'delete-text') {
|
|
1327
|
+
const from = mapping.map(c.from, -1);
|
|
1328
|
+
const to = mapping.map(c.to, 1);
|
|
1329
|
+
const node = newTr.doc.nodeAt(mapping.map(c.pos));
|
|
1330
|
+
if (node === null || node === void 0 ? void 0 : node.marks.find((m) => m.type === schema.marks.tracked_insert)) {
|
|
1331
|
+
newTr.replaceWith(from, to, prosemirrorModel.Fragment.empty);
|
|
1332
|
+
mergeTrackedMarks(from, newTr.doc, newTr, schema);
|
|
1333
|
+
}
|
|
1334
|
+
else {
|
|
1335
|
+
const leftNode = newTr.doc.resolve(from).nodeBefore;
|
|
1336
|
+
const leftMarks = getMergeableMarkTrackedAttrs(leftNode, deleteAttrs, schema);
|
|
1337
|
+
const rightNode = newTr.doc.resolve(to).nodeAfter;
|
|
1338
|
+
const rightMarks = getMergeableMarkTrackedAttrs(rightNode, deleteAttrs, schema);
|
|
1339
|
+
const fromStartOfMark = from - (leftNode && leftMarks ? leftNode.nodeSize : 0);
|
|
1340
|
+
const toEndOfMark = to + (rightNode && rightMarks ? rightNode.nodeSize : 0);
|
|
1341
|
+
const dataTracked = addTrackIdIfDoesntExist({
|
|
1342
|
+
...leftMarks,
|
|
1343
|
+
...rightMarks,
|
|
1344
|
+
...deleteAttrs,
|
|
1345
|
+
});
|
|
1346
|
+
newTr.addMark(fromStartOfMark, toEndOfMark, schema.marks.tracked_delete.create({
|
|
1347
|
+
dataTracked,
|
|
1348
|
+
}));
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
else if (c.type === 'merge-fragment') {
|
|
1352
|
+
let insertPos = mapping.map(c.mergePos);
|
|
1353
|
+
// The default insert position for block nodes is either the start of the merged content or the end.
|
|
1354
|
+
// Incase text was merged, this must be updated as the start or end of the node doesn't map to the
|
|
1355
|
+
// actual position of the merge. Currently the inserted content is inserted at the start or end
|
|
1356
|
+
// of the merged content, TODO reverse the start/end when end/start token?
|
|
1357
|
+
if (c.node.isText) {
|
|
1358
|
+
// When merging text we must delete text in the same go as well, as the from/to boundary goes through
|
|
1359
|
+
// the text node.
|
|
1360
|
+
insertPos = deleteTextIfInserted(c.node, mapping.map(c.pos), newTr, schema, deleteAttrs, mapping.map(c.from), mapping.map(c.to));
|
|
1361
|
+
const newestStep = newTr.steps[newTr.steps.length - 1];
|
|
1362
|
+
if (step !== newestStep) {
|
|
1363
|
+
mapping.appendMap(newestStep.getMap());
|
|
1364
|
+
step = newestStep;
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
if (c.fragment.size > 0) {
|
|
1368
|
+
newTr.insert(insertPos, c.fragment);
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
else if (c.type === 'insert-slice') {
|
|
1372
|
+
const newStep = new prosemirrorTransform.ReplaceStep(mapping.map(c.from), mapping.map(c.to), c.slice, false);
|
|
1189
1373
|
const stepResult = newTr.maybeStep(newStep);
|
|
1190
1374
|
if (stepResult.failed) {
|
|
1191
1375
|
log.error(`insert ReplaceStep failed: "${stepResult.failed}"`, newStep);
|
|
1192
1376
|
return;
|
|
1193
1377
|
}
|
|
1194
|
-
|
|
1195
|
-
mergeTrackedMarks(
|
|
1196
|
-
|
|
1197
|
-
selectionPos = adjustedInsertPos + insertedSlice.size;
|
|
1378
|
+
mergeTrackedMarks(mapping.map(c.from), newTr.doc, newTr, schema);
|
|
1379
|
+
mergeTrackedMarks(mapping.map(c.to), newTr.doc, newTr, schema);
|
|
1380
|
+
selectionPos = mapping.map(c.to) + c.slice.size;
|
|
1198
1381
|
}
|
|
1199
|
-
else {
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1382
|
+
else if (c.type === 'update-node-attrs') {
|
|
1383
|
+
const oldDataTracked = c.oldAttrs.dataTracked;
|
|
1384
|
+
const oldAttrs = (oldDataTracked === null || oldDataTracked === void 0 ? void 0 : oldDataTracked.operation) === exports.CHANGE_OPERATION.set_node_attributes
|
|
1385
|
+
? oldDataTracked.oldAttrs
|
|
1386
|
+
: c.oldAttrs;
|
|
1387
|
+
const dataTracked = addTrackIdIfDoesntExist({
|
|
1388
|
+
...oldDataTracked,
|
|
1389
|
+
oldAttrs,
|
|
1390
|
+
...emptyAttrs,
|
|
1391
|
+
operation: exports.CHANGE_OPERATION.set_node_attributes,
|
|
1392
|
+
});
|
|
1393
|
+
newTr.setNodeMarkup(mapping.map(c.pos), undefined, { ...c.newAttrs, dataTracked });
|
|
1394
|
+
}
|
|
1395
|
+
const newestStep = newTr.steps[newTr.steps.length - 1];
|
|
1396
|
+
if (step !== newestStep) {
|
|
1397
|
+
mapping.appendMap(newestStep.getMap());
|
|
1398
|
+
}
|
|
1399
|
+
});
|
|
1400
|
+
return [mapping, selectionPos];
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
/*!
|
|
1404
|
+
* © 2021 Atypon Systems LLC
|
|
1405
|
+
*
|
|
1406
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
1407
|
+
* you may not use this file except in compliance with the License.
|
|
1408
|
+
* You may obtain a copy of the License at
|
|
1409
|
+
*
|
|
1410
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
1411
|
+
*
|
|
1412
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
1413
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
1414
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
1415
|
+
* See the License for the specific language governing permissions and
|
|
1416
|
+
* limitations under the License.
|
|
1417
|
+
*/
|
|
1418
|
+
function matchInserted(inDeleted, deleted, inserted, newTr, schema) {
|
|
1419
|
+
var _a;
|
|
1420
|
+
for (let i = 0;; i += 1) {
|
|
1421
|
+
if (inserted.childCount === i)
|
|
1422
|
+
return [inDeleted, deleted];
|
|
1423
|
+
const child = inserted.child(i);
|
|
1424
|
+
// @ts-ignore
|
|
1425
|
+
let adjDeleted = deleted.find((d) => (d.type === 'delete-text' && d.to === inDeleted) ||
|
|
1426
|
+
(d.type === 'delete-node' && d.nodeEnd === inDeleted));
|
|
1427
|
+
if (child.type !== ((_a = adjDeleted === null || adjDeleted === void 0 ? void 0 : adjDeleted.node) === null || _a === void 0 ? void 0 : _a.type)) {
|
|
1428
|
+
return [inDeleted, deleted];
|
|
1429
|
+
}
|
|
1430
|
+
else if (child.isText && (adjDeleted === null || adjDeleted === void 0 ? void 0 : adjDeleted.node)) {
|
|
1431
|
+
adjDeleted = adjDeleted;
|
|
1432
|
+
const { pos, from, to, node } = adjDeleted;
|
|
1433
|
+
let j = 0, d = from - pos, maxSteps = Math.max(pos, from) - to;
|
|
1434
|
+
// Match text inside the inserted text node to the deleted text node
|
|
1435
|
+
for (; maxSteps !== j && child.text[j] === node.text[d]; j += 1, d += 1) {
|
|
1436
|
+
inDeleted -= 1;
|
|
1437
|
+
}
|
|
1438
|
+
// this is needed incase diffing tr.doc
|
|
1439
|
+
// deleted.push({
|
|
1440
|
+
// pos: pos,
|
|
1441
|
+
// type: 'update-node-attrs',
|
|
1442
|
+
// // Should check the attrs for equality in fixInconsistentChanges? to remove dataTracked completely
|
|
1443
|
+
// oldAttrs: adjDeleted.node.attrs || {},
|
|
1444
|
+
// newAttrs: child.attrs || {},
|
|
1445
|
+
// })
|
|
1446
|
+
if (maxSteps !== j) {
|
|
1447
|
+
deleted.push({
|
|
1448
|
+
pos,
|
|
1449
|
+
from: Math.max(pos, from) + j,
|
|
1450
|
+
to,
|
|
1451
|
+
type: 'delete-text',
|
|
1452
|
+
node,
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
return [inDeleted, deleted.filter((d) => d !== adjDeleted)];
|
|
1456
|
+
}
|
|
1457
|
+
else if (child.content.size > 0 || (adjDeleted === null || adjDeleted === void 0 ? void 0 : adjDeleted.node.content.size) > 0) {
|
|
1458
|
+
// Move the inDeleted inside the block node's boundary
|
|
1459
|
+
return matchInserted(inDeleted - 1, deleted.filter((d) => d !== adjDeleted), child.content);
|
|
1460
|
+
}
|
|
1461
|
+
deleted.push({
|
|
1462
|
+
pos: adjDeleted.pos,
|
|
1463
|
+
type: 'update-node-attrs',
|
|
1464
|
+
// Should check the attrs for equality in fixInconsistentChanges? to remove dataTracked completely
|
|
1465
|
+
oldAttrs: adjDeleted.node.attrs || {},
|
|
1466
|
+
newAttrs: child.attrs || {},
|
|
1467
|
+
});
|
|
1468
|
+
deleted = deleted.filter((d) => d !== adjDeleted);
|
|
1469
|
+
inDeleted -= child.nodeSize;
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
function diffChangeSteps(deleted, inserted, newTr, schema) {
|
|
1473
|
+
const updated = [];
|
|
1474
|
+
let updatedDeleted = [...deleted];
|
|
1475
|
+
inserted.forEach((ins) => {
|
|
1476
|
+
const [inDeleted, updatedDel] = matchInserted(ins.from, updatedDeleted, ins.slice.content);
|
|
1477
|
+
if (inDeleted === ins.from) {
|
|
1478
|
+
updated.push(ins);
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
1481
|
+
updatedDeleted = updatedDel;
|
|
1482
|
+
const newInsertedA = ins.slice.content.cut(ins.from - inDeleted);
|
|
1483
|
+
const newInsertedB = ins.slice.content.cut(ins.from - inDeleted + 1);
|
|
1484
|
+
// Super hax to cut over block node boundaries in the inserted fragment
|
|
1485
|
+
const newInserted = newInsertedA.size === newInsertedB.size + 2 ? newInsertedB : newInsertedA;
|
|
1486
|
+
if (newInserted.size > 0) {
|
|
1487
|
+
updated.push({
|
|
1488
|
+
...ins,
|
|
1489
|
+
slice: new prosemirrorModel.Slice(newInserted, ins.slice.openStart, ins.slice.openEnd),
|
|
1490
|
+
});
|
|
1203
1491
|
}
|
|
1204
1492
|
});
|
|
1205
|
-
return
|
|
1493
|
+
return [...updatedDeleted, ...updated];
|
|
1206
1494
|
}
|
|
1207
1495
|
|
|
1208
1496
|
/**
|
|
@@ -1226,15 +1514,16 @@ const getSelectionStaticConstructor = (sel) => Object.getPrototypeOf(sel).constr
|
|
|
1226
1514
|
* @param tr Original transaction
|
|
1227
1515
|
* @param oldState State before transaction
|
|
1228
1516
|
* @param newTr Transaction created from the new editor state
|
|
1229
|
-
* @param
|
|
1517
|
+
* @param authorID User id
|
|
1230
1518
|
* @returns newTr that inverts the initial tr and applies track attributes/marks
|
|
1231
1519
|
*/
|
|
1232
|
-
function trackTransaction(tr, oldState, newTr,
|
|
1233
|
-
var _a;
|
|
1520
|
+
function trackTransaction(tr, oldState, newTr, authorID) {
|
|
1234
1521
|
const emptyAttrs = {
|
|
1235
|
-
|
|
1522
|
+
authorID,
|
|
1523
|
+
reviewedByID: null,
|
|
1236
1524
|
createdAt: tr.time,
|
|
1237
|
-
|
|
1525
|
+
updatedAt: tr.time,
|
|
1526
|
+
status: exports.CHANGE_STATUS.pending,
|
|
1238
1527
|
};
|
|
1239
1528
|
// Must use constructor.name instead of instanceof as aliasing prosemirror-state is a lot more
|
|
1240
1529
|
// difficult than prosemirror-transform
|
|
@@ -1249,13 +1538,20 @@ function trackTransaction(tr, oldState, newTr, userID) {
|
|
|
1249
1538
|
'This is probably an error with the library, please report back to maintainers with a reproduction if possible', newTr);
|
|
1250
1539
|
return;
|
|
1251
1540
|
}
|
|
1252
|
-
else if (!(step instanceof ReplaceStep) && step.constructor.name === 'ReplaceStep') {
|
|
1541
|
+
else if (!(step instanceof prosemirrorTransform.ReplaceStep) && step.constructor.name === 'ReplaceStep') {
|
|
1253
1542
|
console.error('@manuscripts/track-changes-plugin: Multiple prosemirror-transform packages imported, alias/dedupe them ' +
|
|
1254
1543
|
'or instanceof checks fail as well as creating new steps');
|
|
1255
1544
|
return;
|
|
1256
1545
|
}
|
|
1257
|
-
else if (step instanceof ReplaceStep) {
|
|
1258
|
-
|
|
1546
|
+
else if (step instanceof prosemirrorTransform.ReplaceStep) {
|
|
1547
|
+
let [steps, startPos] = trackReplaceStep(step, oldState, newTr, emptyAttrs);
|
|
1548
|
+
log.info('CHANGES: ', steps);
|
|
1549
|
+
// deleted and merged really...
|
|
1550
|
+
const deleted = steps.filter((s) => s.type !== 'insert-slice');
|
|
1551
|
+
const inserted = steps.filter((s) => s.type === 'insert-slice');
|
|
1552
|
+
steps = diffChangeSteps(deleted, inserted, newTr, oldState.schema);
|
|
1553
|
+
log.info('DIFFED STEPS: ', steps);
|
|
1554
|
+
const [mapping, selectionPos] = processChangeSteps(steps, startPos, newTr, emptyAttrs, oldState.schema);
|
|
1259
1555
|
if (!wasNodeSelection) {
|
|
1260
1556
|
const sel = getSelectionStaticConstructor(tr.selection);
|
|
1261
1557
|
// Use Selection.near to fix selections that point to a block node instead of inline content
|
|
@@ -1265,11 +1561,17 @@ function trackTransaction(tr, oldState, newTr, userID) {
|
|
|
1265
1561
|
newTr.setSelection(near);
|
|
1266
1562
|
}
|
|
1267
1563
|
}
|
|
1268
|
-
else if (step instanceof ReplaceAroundStep) {
|
|
1269
|
-
trackReplaceAroundStep(step, oldState, newTr, emptyAttrs);
|
|
1270
|
-
|
|
1271
|
-
|
|
1564
|
+
else if (step instanceof prosemirrorTransform.ReplaceAroundStep) {
|
|
1565
|
+
let steps = trackReplaceAroundStep(step, oldState, newTr, emptyAttrs);
|
|
1566
|
+
const deleted = steps.filter((s) => s.type !== 'insert-slice');
|
|
1567
|
+
const inserted = steps.filter((s) => s.type === 'insert-slice');
|
|
1568
|
+
log.info('INSERT STEPS: ', inserted);
|
|
1569
|
+
steps = diffChangeSteps(deleted, inserted, newTr, oldState.schema);
|
|
1570
|
+
log.info('DIFFED STEPS: ', steps);
|
|
1571
|
+
processChangeSteps(steps, tr.selection.from, newTr, emptyAttrs, oldState.schema);
|
|
1272
1572
|
}
|
|
1573
|
+
// } else if (step instanceof AddMarkStep) {
|
|
1574
|
+
// } else if (step instanceof RemoveMarkStep) {
|
|
1273
1575
|
// TODO: here we could check whether adjacent inserts & deletes cancel each other out.
|
|
1274
1576
|
// However, this should not be done by diffing and only matching node or char by char instead since
|
|
1275
1577
|
// it's A easier and B more intuitive to user.
|
|
@@ -1281,24 +1583,24 @@ function trackTransaction(tr, oldState, newTr, userID) {
|
|
|
1281
1583
|
tr.getMeta('uiEvent') && newTr.setMeta('uiEvent', tr.getMeta('uiEvent'));
|
|
1282
1584
|
});
|
|
1283
1585
|
// This is kinda hacky solution at the moment to maintain NodeSelections over transactions
|
|
1284
|
-
// These are required by at least cross-references
|
|
1586
|
+
// These are required by at least cross-references and links to activate their selector pop-ups
|
|
1285
1587
|
if (wasNodeSelection) {
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
const
|
|
1588
|
+
// And -1 here is necessary to keep the selection pointing at the start of the node
|
|
1589
|
+
// (or something, breaks with cross-references otherwise)
|
|
1590
|
+
const mappedPos = newTr.mapping.map(tr.selection.from, -1);
|
|
1289
1591
|
const sel = getSelectionStaticConstructor(tr.selection);
|
|
1290
|
-
newTr.setSelection(sel.create(newTr.doc,
|
|
1592
|
+
newTr.setSelection(sel.create(newTr.doc, mappedPos));
|
|
1291
1593
|
}
|
|
1292
1594
|
log.info('NEW transaction', newTr);
|
|
1293
1595
|
return newTr;
|
|
1294
1596
|
}
|
|
1295
1597
|
|
|
1296
|
-
|
|
1598
|
+
exports.TrackChangesStatus = void 0;
|
|
1297
1599
|
(function (TrackChangesStatus) {
|
|
1298
1600
|
TrackChangesStatus["enabled"] = "enabled";
|
|
1299
1601
|
TrackChangesStatus["viewSnapshots"] = "view-snapshots";
|
|
1300
1602
|
TrackChangesStatus["disabled"] = "disabled";
|
|
1301
|
-
})(TrackChangesStatus || (TrackChangesStatus = {}));
|
|
1603
|
+
})(exports.TrackChangesStatus || (exports.TrackChangesStatus = {}));
|
|
1302
1604
|
|
|
1303
1605
|
/*!
|
|
1304
1606
|
* © 2021 Atypon Systems LLC
|
|
@@ -1315,12 +1617,7 @@ var TrackChangesStatus;
|
|
|
1315
1617
|
* See the License for the specific language governing permissions and
|
|
1316
1618
|
* limitations under the License.
|
|
1317
1619
|
*/
|
|
1318
|
-
const trackChangesPluginKey = new PluginKey('track-changes');
|
|
1319
|
-
// TODO remove
|
|
1320
|
-
const infiniteLoopCounter = {
|
|
1321
|
-
start: 0,
|
|
1322
|
-
iters: 0,
|
|
1323
|
-
};
|
|
1620
|
+
const trackChangesPluginKey = new prosemirrorState.PluginKey('track-changes');
|
|
1324
1621
|
/**
|
|
1325
1622
|
* The ProseMirror plugin needed to enable track-changes.
|
|
1326
1623
|
*
|
|
@@ -1333,18 +1630,18 @@ const trackChangesPlugin = (opts = { userID: 'anonymous:Anonymous' }) => {
|
|
|
1333
1630
|
if (debug) {
|
|
1334
1631
|
enableDebug(true);
|
|
1335
1632
|
}
|
|
1336
|
-
return new Plugin({
|
|
1633
|
+
return new prosemirrorState.Plugin({
|
|
1337
1634
|
key: trackChangesPluginKey,
|
|
1338
1635
|
props: {
|
|
1339
1636
|
editable(state) {
|
|
1340
1637
|
var _a;
|
|
1341
|
-
return ((_a = trackChangesPluginKey.getState(state)) === null || _a === void 0 ? void 0 : _a.status) !== TrackChangesStatus.viewSnapshots;
|
|
1638
|
+
return ((_a = trackChangesPluginKey.getState(state)) === null || _a === void 0 ? void 0 : _a.status) !== exports.TrackChangesStatus.viewSnapshots;
|
|
1342
1639
|
},
|
|
1343
1640
|
},
|
|
1344
1641
|
state: {
|
|
1345
1642
|
init(_config, state) {
|
|
1346
1643
|
return {
|
|
1347
|
-
status: TrackChangesStatus.enabled,
|
|
1644
|
+
status: exports.TrackChangesStatus.enabled,
|
|
1348
1645
|
userID,
|
|
1349
1646
|
changeSet: findChanges(state),
|
|
1350
1647
|
};
|
|
@@ -1362,7 +1659,7 @@ const trackChangesPlugin = (opts = { userID: 'anonymous:Anonymous' }) => {
|
|
|
1362
1659
|
changeSet: findChanges(newState),
|
|
1363
1660
|
};
|
|
1364
1661
|
}
|
|
1365
|
-
else if (pluginState.status === TrackChangesStatus.disabled) {
|
|
1662
|
+
else if (pluginState.status === exports.TrackChangesStatus.disabled) {
|
|
1366
1663
|
return { ...pluginState, changeSet: new ChangeSet() };
|
|
1367
1664
|
}
|
|
1368
1665
|
let { changeSet, ...rest } = pluginState;
|
|
@@ -1386,18 +1683,10 @@ const trackChangesPlugin = (opts = { userID: 'anonymous:Anonymous' }) => {
|
|
|
1386
1683
|
appendTransaction(trs, oldState, newState) {
|
|
1387
1684
|
const pluginState = trackChangesPluginKey.getState(newState);
|
|
1388
1685
|
if (!pluginState ||
|
|
1389
|
-
pluginState.status === TrackChangesStatus.disabled ||
|
|
1686
|
+
pluginState.status === exports.TrackChangesStatus.disabled ||
|
|
1390
1687
|
!(editorView === null || editorView === void 0 ? void 0 : editorView.editable)) {
|
|
1391
1688
|
return null;
|
|
1392
1689
|
}
|
|
1393
|
-
if (infiniteLoopCounter.start < Date.now() - 10000) {
|
|
1394
|
-
infiniteLoopCounter.start = Date.now();
|
|
1395
|
-
infiniteLoopCounter.iters = 0;
|
|
1396
|
-
}
|
|
1397
|
-
if (infiniteLoopCounter.iters >= 100) {
|
|
1398
|
-
console.error('Detected probable infinite loop in track changes!');
|
|
1399
|
-
return null;
|
|
1400
|
-
}
|
|
1401
1690
|
const { userID, changeSet } = pluginState;
|
|
1402
1691
|
let createdTr = newState.tr, docChanged = false;
|
|
1403
1692
|
log.info('TRS', trs);
|
|
@@ -1408,7 +1697,6 @@ const trackChangesPlugin = (opts = { userID: 'anonymous:Anonymous' }) => {
|
|
|
1408
1697
|
(wasAppended && getAction(wasAppended, TrackChangesAction.skipTrack));
|
|
1409
1698
|
if (tr.docChanged && !skipMetaUsed && !skipTrackUsed && !tr.getMeta('history$')) {
|
|
1410
1699
|
createdTr = trackTransaction(tr, oldState, createdTr, userID);
|
|
1411
|
-
infiniteLoopCounter.iters += 1;
|
|
1412
1700
|
}
|
|
1413
1701
|
docChanged = docChanged || tr.docChanged;
|
|
1414
1702
|
const setChangeStatuses = getAction(tr, TrackChangesAction.setChangeStatuses);
|
|
@@ -1417,7 +1705,7 @@ const trackChangesPlugin = (opts = { userID: 'anonymous:Anonymous' }) => {
|
|
|
1417
1705
|
ids.forEach((changeId) => {
|
|
1418
1706
|
const change = changeSet === null || changeSet === void 0 ? void 0 : changeSet.get(changeId);
|
|
1419
1707
|
if (change) {
|
|
1420
|
-
createdTr = updateChangeAttrs(createdTr, change, { status }, oldState.schema);
|
|
1708
|
+
createdTr = updateChangeAttrs(createdTr, change, { status, reviewedByID: userID }, oldState.schema);
|
|
1421
1709
|
setAction(createdTr, TrackChangesAction.updateChanges, [change.id]);
|
|
1422
1710
|
}
|
|
1423
1711
|
});
|
|
@@ -1442,21 +1730,6 @@ const trackChangesPlugin = (opts = { userID: 'anonymous:Anonymous' }) => {
|
|
|
1442
1730
|
});
|
|
1443
1731
|
};
|
|
1444
1732
|
|
|
1445
|
-
/*!
|
|
1446
|
-
* © 2021 Atypon Systems LLC
|
|
1447
|
-
*
|
|
1448
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
1449
|
-
* you may not use this file except in compliance with the License.
|
|
1450
|
-
* You may obtain a copy of the License at
|
|
1451
|
-
*
|
|
1452
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
1453
|
-
*
|
|
1454
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
1455
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
1456
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
1457
|
-
* See the License for the specific language governing permissions and
|
|
1458
|
-
* limitations under the License.
|
|
1459
|
-
*/
|
|
1460
1733
|
/**
|
|
1461
1734
|
* Sets track-changes plugin's status to any of: 'enabled' 'disabled' 'viewSnapshots'. Passing undefined will
|
|
1462
1735
|
* set 'enabled' status to 'disabled' and 'disabled' | 'viewSnapshots' status to 'enabled'.
|
|
@@ -1473,9 +1746,9 @@ const setTrackingStatus = (status) => (state, dispatch) => {
|
|
|
1473
1746
|
let newStatus = status;
|
|
1474
1747
|
if (newStatus === undefined) {
|
|
1475
1748
|
newStatus =
|
|
1476
|
-
currentStatus === TrackChangesStatus.enabled
|
|
1477
|
-
? TrackChangesStatus.disabled
|
|
1478
|
-
: TrackChangesStatus.enabled;
|
|
1749
|
+
currentStatus === exports.TrackChangesStatus.enabled
|
|
1750
|
+
? exports.TrackChangesStatus.disabled
|
|
1751
|
+
: exports.TrackChangesStatus.enabled;
|
|
1479
1752
|
}
|
|
1480
1753
|
dispatch && dispatch(setAction(state.tr, TrackChangesAction.setPluginStatus, newStatus));
|
|
1481
1754
|
return true;
|
|
@@ -1544,4 +1817,9 @@ var commands = /*#__PURE__*/Object.freeze({
|
|
|
1544
1817
|
setParagraphTestAttribute: setParagraphTestAttribute
|
|
1545
1818
|
});
|
|
1546
1819
|
|
|
1547
|
-
|
|
1820
|
+
exports.ChangeSet = ChangeSet;
|
|
1821
|
+
exports.enableDebug = enableDebug;
|
|
1822
|
+
exports.skipTracking = skipTracking;
|
|
1823
|
+
exports.trackChangesPlugin = trackChangesPlugin;
|
|
1824
|
+
exports.trackChangesPluginKey = trackChangesPluginKey;
|
|
1825
|
+
exports.trackCommands = commands;
|