@manuscripts/track-changes-plugin 0.0.3 → 0.2.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/dist/index.cjs ADDED
@@ -0,0 +1,1562 @@
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);
13
+
14
+ exports.TrackChangesAction = void 0;
15
+ (function (TrackChangesAction) {
16
+ TrackChangesAction["skipTrack"] = "track-changes-skip-tracking";
17
+ TrackChangesAction["setUserID"] = "track-changes-set-user-id";
18
+ TrackChangesAction["setPluginStatus"] = "track-changes-set-track-status";
19
+ TrackChangesAction["setChangeStatuses"] = "track-changes-set-change-statuses";
20
+ TrackChangesAction["updateChanges"] = "track-changes-update-changes";
21
+ TrackChangesAction["refreshChanges"] = "track-changes-refresh-changes";
22
+ TrackChangesAction["applyAndRemoveChanges"] = "track-changes-apply-remove-changes";
23
+ })(exports.TrackChangesAction || (exports.TrackChangesAction = {}));
24
+ /**
25
+ * Gets the value of a meta field, action payload, of a defined track-changes action.
26
+ * @param tr
27
+ * @param action
28
+ */
29
+ function getAction(tr, action) {
30
+ return tr.getMeta(action);
31
+ }
32
+ /**
33
+ * Use this function to set meta keys to transactions that are consumed by the track-changes-plugin.
34
+ * For example, you can skip tracking of a transaction with setAction(tr, TrackChangesAction.skipTrack, true)
35
+ * @param tr
36
+ * @param action
37
+ * @param payload
38
+ */
39
+ function setAction(tr, action, payload) {
40
+ return tr.setMeta(action, payload);
41
+ }
42
+
43
+ /******************************************************************************
44
+ Copyright (c) Microsoft Corporation.
45
+
46
+ Permission to use, copy, modify, and/or distribute this software for any
47
+ purpose with or without fee is hereby granted.
48
+
49
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
50
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
51
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
52
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
53
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
54
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
55
+ PERFORMANCE OF THIS SOFTWARE.
56
+ ***************************************************************************** */
57
+
58
+ function __classPrivateFieldGet(receiver, state, kind, f) {
59
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
60
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
61
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
62
+ }
63
+
64
+ function __classPrivateFieldSet(receiver, state, value, kind, f) {
65
+ if (kind === "m") throw new TypeError("Private method is not writable");
66
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
67
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
68
+ return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
69
+ }
70
+
71
+ /*!
72
+ * © 2021 Atypon Systems LLC
73
+ *
74
+ * Licensed under the Apache License, Version 2.0 (the "License");
75
+ * you may not use this file except in compliance with the License.
76
+ * You may obtain a copy of the License at
77
+ *
78
+ * http://www.apache.org/licenses/LICENSE-2.0
79
+ *
80
+ * Unless required by applicable law or agreed to in writing, software
81
+ * distributed under the License is distributed on an "AS IS" BASIS,
82
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
83
+ * See the License for the specific language governing permissions and
84
+ * limitations under the License.
85
+ */
86
+ exports.CHANGE_OPERATION = void 0;
87
+ (function (CHANGE_OPERATION) {
88
+ CHANGE_OPERATION["insert"] = "insert";
89
+ CHANGE_OPERATION["delete"] = "delete";
90
+ CHANGE_OPERATION["set_node_attributes"] = "set_node_attributes";
91
+ CHANGE_OPERATION["wrap_with_node"] = "wrap_with_node";
92
+ CHANGE_OPERATION["unwrap_from_node"] = "unwrap_from_node";
93
+ CHANGE_OPERATION["add_mark"] = "add_mark";
94
+ CHANGE_OPERATION["remove_mark"] = "remove_mark";
95
+ })(exports.CHANGE_OPERATION || (exports.CHANGE_OPERATION = {}));
96
+ exports.CHANGE_STATUS = void 0;
97
+ (function (CHANGE_STATUS) {
98
+ CHANGE_STATUS["accepted"] = "accepted";
99
+ CHANGE_STATUS["rejected"] = "rejected";
100
+ CHANGE_STATUS["pending"] = "pending";
101
+ })(exports.CHANGE_STATUS || (exports.CHANGE_STATUS = {}));
102
+
103
+ /*!
104
+ * © 2021 Atypon Systems LLC
105
+ *
106
+ * Licensed under the Apache License, Version 2.0 (the "License");
107
+ * you may not use this file except in compliance with the License.
108
+ * You may obtain a copy of the License at
109
+ *
110
+ * http://www.apache.org/licenses/LICENSE-2.0
111
+ *
112
+ * Unless required by applicable law or agreed to in writing, software
113
+ * distributed under the License is distributed on an "AS IS" BASIS,
114
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
115
+ * See the License for the specific language governing permissions and
116
+ * limitations under the License.
117
+ */
118
+ const logger = debug__default["default"]('track');
119
+ const log = {
120
+ info(str, obj) {
121
+ if (obj) {
122
+ logger(str, obj);
123
+ }
124
+ else {
125
+ logger(str);
126
+ }
127
+ },
128
+ warn(str, obj) {
129
+ if (obj) {
130
+ logger(`%c WARNING ${str}`, 'color: #f3f32c', obj);
131
+ }
132
+ else {
133
+ logger(`%c WARNING ${str}`, 'color: #f3f32c');
134
+ }
135
+ },
136
+ error(str, obj) {
137
+ if (obj) {
138
+ logger(`%c ERROR ${str}`, 'color: #ff4242', obj);
139
+ }
140
+ else {
141
+ logger(`%c ERROR ${str}`, 'color: #ff4242');
142
+ }
143
+ },
144
+ };
145
+ /**
146
+ * Sets debug logging enabled/disabled.
147
+ * @param enabled
148
+ */
149
+ const enableDebug = (enabled) => {
150
+ if (enabled) {
151
+ debug__default["default"].enable('track');
152
+ }
153
+ else {
154
+ debug__default["default"].disable();
155
+ }
156
+ };
157
+
158
+ var _ChangeSet_changes;
159
+ /**
160
+ * ChangeSet is a data structure to contain the tracked changes with some utility methods and computed
161
+ * values to allow easier operability.
162
+ */
163
+ class ChangeSet {
164
+ constructor(changes = []) {
165
+ _ChangeSet_changes.set(this, void 0);
166
+ __classPrivateFieldSet(this, _ChangeSet_changes, changes, "f");
167
+ }
168
+ /**
169
+ * List of all the valid TrackedChanges. This prevents for example changes with duplicate ids being shown
170
+ * in the UI, causing errors.
171
+ */
172
+ get changes() {
173
+ const iteratedIds = new Set();
174
+ return __classPrivateFieldGet(this, _ChangeSet_changes, "f").filter((c) => {
175
+ const valid = !iteratedIds.has(c.attrs.id) && ChangeSet.isValidTrackedAttrs(c.attrs);
176
+ iteratedIds.add(c.attrs.id);
177
+ return valid;
178
+ });
179
+ }
180
+ get invalidChanges() {
181
+ return __classPrivateFieldGet(this, _ChangeSet_changes, "f").filter((c) => !this.changes.find((cc) => c.id === cc.id));
182
+ }
183
+ /**
184
+ * List of 1-level nested changes where the top-most node change contains all the changes within its start
185
+ * and end position. This is useful for showing the changes as groups in the UI.
186
+ */
187
+ get changeTree() {
188
+ const rootNodes = [];
189
+ let currentNodeChange;
190
+ this.changes.forEach((c) => {
191
+ if (currentNodeChange && c.from >= currentNodeChange.to) {
192
+ rootNodes.push(currentNodeChange);
193
+ currentNodeChange = undefined;
194
+ }
195
+ if (c.type === 'node-change' && currentNodeChange && c.from < currentNodeChange.to) {
196
+ currentNodeChange.children.push(c);
197
+ }
198
+ else if (c.type === 'node-change') {
199
+ currentNodeChange = { ...c, children: [] };
200
+ }
201
+ else if (c.type === 'text-change' && currentNodeChange && c.from < currentNodeChange.to) {
202
+ currentNodeChange.children.push(c);
203
+ }
204
+ else if (c.type === 'text-change') {
205
+ rootNodes.push(c);
206
+ }
207
+ });
208
+ if (currentNodeChange) {
209
+ rootNodes.push(currentNodeChange);
210
+ }
211
+ return rootNodes;
212
+ }
213
+ get pending() {
214
+ return this.changeTree.filter((c) => c.attrs.status === exports.CHANGE_STATUS.pending);
215
+ }
216
+ get accepted() {
217
+ return this.changeTree.filter((c) => c.attrs.status === exports.CHANGE_STATUS.accepted);
218
+ }
219
+ get rejected() {
220
+ return this.changeTree.filter((c) => c.attrs.status === exports.CHANGE_STATUS.rejected);
221
+ }
222
+ get textChanges() {
223
+ return this.changes.filter((c) => c.type === 'text-change');
224
+ }
225
+ get nodeChanges() {
226
+ return this.changes.filter((c) => c.type === 'node-change');
227
+ }
228
+ get isEmpty() {
229
+ return __classPrivateFieldGet(this, _ChangeSet_changes, "f").length === 0;
230
+ }
231
+ /**
232
+ * Used to determine whether `fixInconsistentChanges` has to be executed to replace eg duplicate ids or
233
+ * changes that are missing attributes.
234
+ */
235
+ get hasInconsistentData() {
236
+ return this.hasDuplicateIds || this.hasIncompleteAttrs;
237
+ }
238
+ get hasDuplicateIds() {
239
+ const iterated = new Set();
240
+ return __classPrivateFieldGet(this, _ChangeSet_changes, "f").some((c) => {
241
+ if (iterated.has(c.id)) {
242
+ return true;
243
+ }
244
+ iterated.add(c.id);
245
+ });
246
+ }
247
+ get hasIncompleteAttrs() {
248
+ return __classPrivateFieldGet(this, _ChangeSet_changes, "f").some((c) => !ChangeSet.isValidTrackedAttrs(c.attrs));
249
+ }
250
+ get(id) {
251
+ return __classPrivateFieldGet(this, _ChangeSet_changes, "f").find((c) => c.id === id);
252
+ }
253
+ getIn(ids) {
254
+ return ids
255
+ .map((id) => __classPrivateFieldGet(this, _ChangeSet_changes, "f").find((c) => c.id === id))
256
+ .filter((c) => c !== undefined);
257
+ }
258
+ getNotIn(ids) {
259
+ return __classPrivateFieldGet(this, _ChangeSet_changes, "f").filter((c) => ids.includes(c.id));
260
+ }
261
+ /**
262
+ * Flattens a changeTree into a list of IDs
263
+ * @param changes
264
+ */
265
+ static flattenTreeToIds(changes) {
266
+ return changes.flatMap((c) => this.isNodeChange(c) ? [c.id, ...c.children.map((c) => c.id)] : c.id);
267
+ }
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
+ /**
278
+ * Determines whether a change should be deleted when applying it to the document.
279
+ * @param change
280
+ */
281
+ static shouldDeleteChange(change) {
282
+ const { status, operation } = change.attrs;
283
+ return ((operation === exports.CHANGE_OPERATION.insert && status === exports.CHANGE_STATUS.rejected) ||
284
+ (operation === exports.CHANGE_OPERATION.delete && status === exports.CHANGE_STATUS.accepted));
285
+ }
286
+ /**
287
+ * Checks whether change attributes contain all TrackedAttrs keys with non-undefined values
288
+ * @param attrs
289
+ */
290
+ static isValidTrackedAttrs(attrs = {}) {
291
+ if ('attrs' in attrs) {
292
+ log.warn('passed "attrs" as property to isValidTrackedAttrs(attrs)', attrs);
293
+ }
294
+ const trackedKeys = ['id', 'userID', 'operation', 'status', 'createdAt'];
295
+ const entries = Object.entries(attrs);
296
+ return (entries.length === trackedKeys.length &&
297
+ entries.every(([key, val]) => trackedKeys.includes(key) && val !== undefined) &&
298
+ (attrs.id || '').length > 0 // Changes created with undefined id have '' as placeholder
299
+ );
300
+ }
301
+ static isTextChange(change) {
302
+ return change.type === 'text-change';
303
+ }
304
+ static isNodeChange(change) {
305
+ return change.type === 'node-change';
306
+ }
307
+ }
308
+ _ChangeSet_changes = new WeakMap();
309
+
310
+ /*!
311
+ * © 2021 Atypon Systems LLC
312
+ *
313
+ * Licensed under the Apache License, Version 2.0 (the "License");
314
+ * you may not use this file except in compliance with the License.
315
+ * You may obtain a copy of the License at
316
+ *
317
+ * http://www.apache.org/licenses/LICENSE-2.0
318
+ *
319
+ * Unless required by applicable law or agreed to in writing, software
320
+ * distributed under the License is distributed on an "AS IS" BASIS,
321
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
322
+ * See the License for the specific language governing permissions and
323
+ * limitations under the License.
324
+ */
325
+ /**
326
+ * Deletes node but tries to leave its content intact by trying to unwrap it first
327
+ *
328
+ * Incase unwrapping doesn't work deletes the whole node.
329
+ * @param node
330
+ * @param pos
331
+ * @param tr
332
+ * @returns
333
+ */
334
+ function deleteNode(node, pos, tr) {
335
+ var _a;
336
+ const startPos = tr.doc.resolve(pos + 1);
337
+ const range = startPos.blockRange(tr.doc.resolve(startPos.pos - 2 + node.nodeSize));
338
+ const targetDepth = range && prosemirrorTransform.liftTarget(range);
339
+ // Check with typeof since with old prosemirror-transform targetDepth is undefined
340
+ if (range && typeof targetDepth === 'number') {
341
+ return tr.lift(range, targetDepth);
342
+ }
343
+ const resPos = tr.doc.resolve(pos);
344
+ // Block nodes can be deleted by just removing their start token which should then merge the text
345
+ // content to above node's content (if there is one)
346
+ const canMergeToNodeAbove = (resPos.parent !== tr.doc || resPos.nodeBefore) && node.isBlock && ((_a = node.firstChild) === null || _a === void 0 ? void 0 : _a.isText);
347
+ if (canMergeToNodeAbove) {
348
+ return tr.replaceWith(pos - 1, pos + 1, prosemirrorModel.Fragment.empty);
349
+ }
350
+ else {
351
+ // NOTE: there's an edge case where moving content is not possible but because the immediate
352
+ // child, say some wrapper blockNode, is also deleted the content could be retained. TODO I guess.
353
+ return tr.delete(pos, pos + node.nodeSize);
354
+ }
355
+ }
356
+
357
+ /*!
358
+ * © 2021 Atypon Systems LLC
359
+ *
360
+ * Licensed under the Apache License, Version 2.0 (the "License");
361
+ * you may not use this file except in compliance with the License.
362
+ * You may obtain a copy of the License at
363
+ *
364
+ * http://www.apache.org/licenses/LICENSE-2.0
365
+ *
366
+ * Unless required by applicable law or agreed to in writing, software
367
+ * distributed under the License is distributed on an "AS IS" BASIS,
368
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
369
+ * See the License for the specific language governing permissions and
370
+ * limitations under the License.
371
+ */
372
+ /**
373
+ * Deletes node but tries to leave its content intact by moving/wrapping it to a node before or after
374
+ * @param node
375
+ * @param pos
376
+ * @param tr
377
+ * @returns
378
+ */
379
+ function mergeNode(node, pos, tr) {
380
+ var _a;
381
+ if (prosemirrorTransform.canJoin(tr.doc, pos)) {
382
+ return tr.join(pos);
383
+ }
384
+ else if (prosemirrorTransform.canJoin(tr.doc, pos + node.nodeSize)) {
385
+ // TODO should copy the attributes from the merged node below
386
+ return tr.join(pos + node.nodeSize);
387
+ }
388
+ // TODO is this the same thing as join to above?
389
+ const resPos = tr.doc.resolve(pos);
390
+ const canMergeToNodeAbove = (resPos.parent !== tr.doc || resPos.nodeBefore) && ((_a = node.firstChild) === null || _a === void 0 ? void 0 : _a.isText);
391
+ if (canMergeToNodeAbove) {
392
+ return tr.replaceWith(pos - 1, pos + 1, prosemirrorModel.Fragment.empty);
393
+ }
394
+ return undefined;
395
+ }
396
+
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
+ function updateChangeAttrs(tr, change, trackedAttrs, schema) {
475
+ const node = tr.doc.nodeAt(change.from);
476
+ if (!node) {
477
+ throw Error('No node at the from of change' + change);
478
+ }
479
+ const dataTracked = { ...getNodeTrackedData(node, schema), ...trackedAttrs };
480
+ const oldMark = node.marks.find((m) => m.type === schema.marks.tracked_insert || m.type === schema.marks.tracked_delete);
481
+ if (change.type === 'text-change' && oldMark) {
482
+ tr.addMark(change.from, change.to, oldMark.type.create({ ...oldMark.attrs, dataTracked }));
483
+ }
484
+ else if (change.type === 'node-change') {
485
+ tr.setNodeMarkup(change.from, undefined, { ...node.attrs, dataTracked }, node.marks);
486
+ }
487
+ return tr;
488
+ }
489
+ function updateChangeChildrenAttributes(changes, tr, mapping) {
490
+ changes.forEach((c) => {
491
+ if (c.type === 'node-change' && ChangeSet.shouldNotDelete(c)) {
492
+ const from = mapping.map(c.from);
493
+ const node = tr.doc.nodeAt(from);
494
+ if (!node) {
495
+ return;
496
+ }
497
+ const attrs = { ...node.attrs, dataTracked: null };
498
+ tr.setNodeMarkup(from, undefined, attrs, node.marks);
499
+ }
500
+ });
501
+ }
502
+
503
+ /**
504
+ * Applies the accepted/rejected changes in the current document and sets them untracked
505
+ *
506
+ * @param tr
507
+ * @param schema
508
+ * @param changes
509
+ * @param deleteMap
510
+ */
511
+ function applyAcceptedRejectedChanges(tr, schema, changes, deleteMap = new prosemirrorTransform.Mapping()) {
512
+ changes.forEach((change) => {
513
+ if (change.attrs.status === exports.CHANGE_STATUS.pending) {
514
+ return;
515
+ }
516
+ // Map change.from and skip those which dont need to be applied
517
+ // 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.shouldNotDelete(change);
519
+ if (!node) {
520
+ !deleted && log.warn('no node found to update for change', change);
521
+ return;
522
+ }
523
+ if (ChangeSet.isTextChange(change) && noChangeNeeded) {
524
+ tr.removeMark(from, deleteMap.map(change.to), schema.marks.tracked_insert);
525
+ tr.removeMark(from, deleteMap.map(change.to), schema.marks.tracked_delete);
526
+ }
527
+ else if (ChangeSet.isTextChange(change)) {
528
+ tr.delete(from, deleteMap.map(change.to));
529
+ deleteMap.appendMap(tr.steps[tr.steps.length - 1].getMap());
530
+ }
531
+ else if (ChangeSet.isNodeChange(change) && noChangeNeeded) {
532
+ const attrs = { ...node.attrs, dataTracked: null };
533
+ tr.setNodeMarkup(from, undefined, attrs, node.marks);
534
+ updateChangeChildrenAttributes(change.children, tr, deleteMap);
535
+ }
536
+ else if (ChangeSet.isNodeChange(change)) {
537
+ // Try first moving the node children to either nodeAbove, nodeBelow or its parent.
538
+ // Then try unwrapping it with lift or just hacky-joining by replacing the border between
539
+ // it and its parent with Fragment.empty. If none of these apply, delete the content between the change.
540
+ const merged = mergeNode(node, from, tr);
541
+ if (merged === undefined) {
542
+ deleteNode(node, from, tr);
543
+ }
544
+ deleteMap.appendMap(tr.steps[tr.steps.length - 1].getMap());
545
+ }
546
+ });
547
+ return deleteMap;
548
+ }
549
+
550
+ /**
551
+ * Finds all changes (basically text marks or node attributes) from document
552
+ *
553
+ * This could be possibly made more efficient by only iterating the sections of doc
554
+ * where changes have been applied. This could attempted with eg findDiffStart
555
+ * but it might be less robust than just using doc.descendants
556
+ * @param state
557
+ * @returns
558
+ */
559
+ function findChanges(state) {
560
+ const changes = [];
561
+ // Store the last iterated change to join adjacent text changes
562
+ let current;
563
+ state.doc.descendants((node, pos) => {
564
+ const attrs = getNodeTrackedData(node, state.schema);
565
+ if (attrs) {
566
+ const id = (attrs === null || attrs === void 0 ? void 0 : attrs.id) || '';
567
+ // Join adjacent text changes that have been broken up due to different marks
568
+ // eg <ins><b>bold</b>norm<i>italic</i></ins> -> treated as one continuous change
569
+ // Note the !equalMarks to leave changes separate incase the marks are equal to let fixInconsistentChanges to fix them
570
+ if (current &&
571
+ current.change.id === id &&
572
+ current.node.isText &&
573
+ node.isText &&
574
+ !equalMarks(node, current.node)) {
575
+ current.change.to = pos + node.nodeSize;
576
+ // Important to update the node as the text changes might contain multiple parts where some marks equal each other
577
+ current.node = node;
578
+ }
579
+ else if (node.isText) {
580
+ current && changes.push(current.change);
581
+ current = {
582
+ change: {
583
+ id,
584
+ type: 'text-change',
585
+ from: pos,
586
+ to: pos + node.nodeSize,
587
+ attrs,
588
+ },
589
+ node,
590
+ };
591
+ }
592
+ else {
593
+ current && changes.push(current.change);
594
+ current = {
595
+ change: {
596
+ id,
597
+ type: 'node-change',
598
+ from: pos,
599
+ to: pos + node.nodeSize,
600
+ nodeType: node.type.name,
601
+ children: [],
602
+ attrs,
603
+ },
604
+ node,
605
+ };
606
+ }
607
+ }
608
+ else if (current) {
609
+ changes.push(current.change);
610
+ current = undefined;
611
+ }
612
+ });
613
+ current && changes.push(current.change);
614
+ return new ChangeSet(changes);
615
+ }
616
+
617
+ /**
618
+ * Iterates over a ChangeSet to check all changes have their required attributes
619
+ *
620
+ * This inconsistency might happen due to a bug in the track changes implementation or by a user somehow applying an empty insert/delete mark that doesn't contain proper data. Also this checks the track IDs for duplicates.
621
+ * @param changeSet
622
+ * @param currentUser
623
+ * @param newTr
624
+ * @param schema
625
+ * @return docWasChanged, a boolean
626
+ */
627
+ function fixInconsistentChanges(changeSet, trackUserID, newTr, schema) {
628
+ const iteratedIds = new Set();
629
+ let changed = false;
630
+ changeSet.invalidChanges.forEach((c) => {
631
+ const { id, userID, operation, status, createdAt } = c.attrs;
632
+ const newAttrs = {
633
+ ...((!id || iteratedIds.has(id) || id.length === 0) && { id: uuidv4() }),
634
+ ...(!userID && { userID: trackUserID }),
635
+ ...(!operation && { operation: exports.CHANGE_OPERATION.insert }),
636
+ ...(!status && { status: exports.CHANGE_STATUS.pending }),
637
+ ...(!createdAt && { createdAt: Date.now() }),
638
+ };
639
+ if (Object.keys(newAttrs).length > 0) {
640
+ updateChangeAttrs(newTr, c, { ...c.attrs, ...newAttrs }, schema);
641
+ changed = true;
642
+ }
643
+ iteratedIds.add(newAttrs.id || id);
644
+ });
645
+ return changed;
646
+ }
647
+
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
+ /*!
730
+ * © 2021 Atypon Systems LLC
731
+ *
732
+ * Licensed under the Apache License, Version 2.0 (the "License");
733
+ * you may not use this file except in compliance with the License.
734
+ * You may obtain a copy of the License at
735
+ *
736
+ * http://www.apache.org/licenses/LICENSE-2.0
737
+ *
738
+ * Unless required by applicable law or agreed to in writing, software
739
+ * distributed under the License is distributed on an "AS IS" BASIS,
740
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
741
+ * See the License for the specific language governing permissions and
742
+ * limitations under the License.
743
+ */
744
+ /**
745
+ * Recurses node children and returns the merged first/last node's content and the unmerged children
746
+ *
747
+ * For example when merging two blockquotes:
748
+ * <bq><p>old|</p></bq>...| + [<bq><p>] inserted</p><p>2nd p</p></bq> -> <bq><p>old inserted</p><p>2nd p</p></bq>
749
+ * The extracted merged and unmerged content from the insertSlice are:
750
+ * {
751
+ * mergedNodeContent: <text> inserted</text>
752
+ * unmergedContent: [<p>2nd p</p>]
753
+ * }
754
+ * @param node
755
+ * @param currentDepth
756
+ * @param depth
757
+ * @param first
758
+ * @returns
759
+ */
760
+ function getMergedNode(node, currentDepth, depth, first) {
761
+ if (currentDepth === depth) {
762
+ return {
763
+ mergedNodeContent: node.content,
764
+ unmergedContent: undefined,
765
+ };
766
+ }
767
+ const result = [];
768
+ let merged = prosemirrorModel.Fragment.empty;
769
+ node.content.forEach((n, _, i) => {
770
+ if ((first && i === 0) || (!first && i === node.childCount - 1)) {
771
+ const { mergedNodeContent, unmergedContent } = getMergedNode(n, currentDepth + 1, depth, first);
772
+ merged = mergedNodeContent;
773
+ if (unmergedContent) {
774
+ result.push(...unmergedContent.content);
775
+ }
776
+ }
777
+ else {
778
+ result.push(n);
779
+ }
780
+ });
781
+ return {
782
+ mergedNodeContent: merged,
783
+ unmergedContent: result.length > 0 ? prosemirrorModel.Fragment.fromArray(result) : undefined,
784
+ };
785
+ }
786
+ /**
787
+ * Filters merged nodes from an open insertSlice to manually merge them to prevent unwanted deletions
788
+ *
789
+ * So instead of joining the slice by its open sides, possibly deleting previous nodes, we can push the
790
+ * changed content manually inside the merged nodes.
791
+ * Eg. instead of doing `|<p>asdf</p><p>|bye</p>` automatically, we extract the merged nodes first:
792
+ * {
793
+ * updatedSliceNodes: [<p>asdf</p>],
794
+ * firstMergedNode: <p>bye</p>,
795
+ * lastMergedNode: undefined,
796
+ * }
797
+ * @param insertSlice inserted slice
798
+ */
799
+ function splitSliceIntoMergedParts(insertSlice, mergeEqualSides = false) {
800
+ const { openStart, openEnd, content: { firstChild, lastChild, content: nodes }, } = insertSlice;
801
+ let updatedSliceNodes = nodes;
802
+ const mergeSides = openStart !== openEnd || mergeEqualSides;
803
+ const firstMergedNode = openStart > 0 && mergeSides && firstChild
804
+ ? getMergedNode(firstChild, 1, openStart, true)
805
+ : undefined;
806
+ const lastMergedNode = openEnd > 0 && mergeSides && lastChild ? getMergedNode(lastChild, 1, openEnd, false) : undefined;
807
+ if (firstMergedNode) {
808
+ updatedSliceNodes = updatedSliceNodes.slice(1);
809
+ if (firstMergedNode.unmergedContent) {
810
+ updatedSliceNodes = [...firstMergedNode.unmergedContent.content, ...updatedSliceNodes];
811
+ }
812
+ }
813
+ if (lastMergedNode) {
814
+ updatedSliceNodes = updatedSliceNodes.slice(0, -1);
815
+ if (lastMergedNode.unmergedContent) {
816
+ updatedSliceNodes = [...updatedSliceNodes, ...lastMergedNode.unmergedContent.content];
817
+ }
818
+ }
819
+ return {
820
+ updatedSliceNodes,
821
+ firstMergedNode,
822
+ lastMergedNode,
823
+ };
824
+ }
825
+ /**
826
+ * Deletes inserted text directly, otherwise wraps it with tracked_delete mark
827
+ *
828
+ * This would work for general inline nodes too, but since node marks don't work properly
829
+ * with Yjs, attributes are used instead.
830
+ * @param node
831
+ * @param pos
832
+ * @param newTr
833
+ * @param schema
834
+ * @param deleteAttrs
835
+ * @param from
836
+ * @param to
837
+ */
838
+ function deleteTextIfInserted(node, pos, newTr, schema, deleteAttrs, from, to) {
839
+ const start = from ? Math.max(pos, from) : pos;
840
+ const nodeEnd = pos + node.nodeSize;
841
+ const end = to ? Math.min(nodeEnd, to) : nodeEnd;
842
+ if (node.marks.find((m) => m.type === schema.marks.tracked_insert)) {
843
+ // Math.max(pos, from) is for picking always the start of the node,
844
+ // not the start of the change (which might span multiple nodes).
845
+ // Pos can be less than from as nodesBetween iterates through all nodes starting from the top block node
846
+ newTr.replaceWith(start, end, prosemirrorModel.Fragment.empty);
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
+ }
866
+ }
867
+ /**
868
+ * Deletes inserted block or inline node, otherwise adds `dataTracked` object with CHANGE_STATUS 'deleted'
869
+ * @param node
870
+ * @param pos
871
+ * @param newTr
872
+ * @param deleteAttrs
873
+ */
874
+ function deleteOrSetNodeDeleted(node, pos, newTr, deleteAttrs) {
875
+ const dataTracked = node.attrs.dataTracked;
876
+ const wasInsertedBySameUser = (dataTracked === null || dataTracked === void 0 ? void 0 : dataTracked.operation) === exports.CHANGE_OPERATION.insert && dataTracked.userID === deleteAttrs.userID;
877
+ if (wasInsertedBySameUser) {
878
+ deleteNode(node, pos, newTr);
879
+ }
880
+ else {
881
+ const attrs = {
882
+ ...node.attrs,
883
+ dataTracked: addTrackIdIfDoesntExist(deleteAttrs),
884
+ };
885
+ newTr.setNodeMarkup(pos, undefined, attrs, node.marks);
886
+ }
887
+ }
888
+ /**
889
+ * Applies deletion to the doc without actually deleting nodes that have not been inserted
890
+ *
891
+ * The hairiest part of this whole library which does a fair bit of magic to split the inserted slice
892
+ * into pieces that can be inserted without deleting nodes in the doc. Basically we first split the
893
+ * inserted slice into merged pieces _if_ the slice was open on either end. Then, we iterate over the deleted
894
+ * range and see if the node in question was completely wrapped in the range (therefore fully deleted)
895
+ * or only partially deleted by the slice. In that case, we merge the content from the inserted slice
896
+ * and keep the original nodes if they do not contain insert attributes.
897
+ *
898
+ * It is definitely a messy function but so far this seems to have been the best approach to prevent
899
+ * deletion of nodes with open slices. Other option would be to allow the deletions to take place but that
900
+ * requires then inserting the deleted nodes back to the doc if their deletion should be prevented, which does
901
+ * not seem trivial either.
902
+ *
903
+ * @param from start of the deleted range
904
+ * @param to end of the deleted range
905
+ * @param gap retained content in a ReplaceAroundStep, not deleted
906
+ * @param startDoc doc before the deletion
907
+ * @param newTr the new track transaction
908
+ * @param schema ProseMirror schema
909
+ * @param deleteAttrs attributes for the dataTracked object
910
+ * @param insertSlice the inserted slice from ReplaceStep
911
+ * @returns mapping adjusted by the applied operations & modified insert slice
912
+ */
913
+ function deleteAndMergeSplitNodes(from, to, gap, startDoc, newTr, schema, trackAttrs, insertSlice) {
914
+ const deleteMap = new prosemirrorTransform.Mapping();
915
+ const mergedInsertPos = undefined;
916
+ // No deletion applied, return default values
917
+ if (from === to) {
918
+ return {
919
+ deleteMap,
920
+ mergedInsertPos,
921
+ newSliceContent: insertSlice.content,
922
+ };
923
+ }
924
+ const { openStart, openEnd } = insertSlice;
925
+ const { updatedSliceNodes, firstMergedNode, lastMergedNode } = splitSliceIntoMergedParts(insertSlice, gap !== undefined);
926
+ const deleteAttrs = createNewDeleteAttrs(trackAttrs);
927
+ let mergingStartSide = true;
928
+ startDoc.nodesBetween(from, to, (node, pos) => {
929
+ const { pos: offsetPos, deleted: nodeWasDeleted } = deleteMap.mapResult(pos, 1);
930
+ const offsetFrom = deleteMap.map(from, -1);
931
+ const offsetTo = deleteMap.map(to, 1);
932
+ const nodeEnd = offsetPos + node.nodeSize;
933
+ // So this insane boolean checks for ReplaceAroundStep gaps and whether the node should be skipped
934
+ // since the content inside gap should stay unchanged.
935
+ // All other nodes except text nodes consist of one start and end token (or just a single token for atoms).
936
+ // For them we can just check whether the start token is within the gap eg pos is 10 when gap (8, 18) to
937
+ // determine whether it should be skipped.
938
+ // For text nodes though, since they are continous, they might only partially be enclosed in the gap
939
+ // eg. pos 10 when gap is (8, 18) BUT if their nodeEnd goes past the gap's end eg nodeEnd 20 they actually
940
+ // are altered and should not be skipped.
941
+ // @TODO ATM 20.7.2022 there doesn't seem to be tests that capture this.
942
+ const wasWithinGap = gap &&
943
+ ((!node.isText && offsetPos >= deleteMap.map(gap.start, -1)) ||
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];
948
+ // nodeEnd > offsetFrom -> delete touches this node
949
+ // 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) {
958
+ // |<p>asdf</p>| -> node deleted completely
959
+ const nodeCompletelyDeleted = offsetPos >= offsetFrom && nodeEnd <= offsetTo;
960
+ // The end token deleted eg:
961
+ // <p 1>asdf|</p 7><p 7>bye</p 12>| + [<p>]hello</p> -> <p>asdfhello</p>
962
+ // (del 6 12) + (ins [<p>]hello</p> openStart 1 openEnd 0)
963
+ // (<p> nodeEnd 7) > (from 6) && (nodeEnd 7) <= (to 12)
964
+ //
965
+ // How about
966
+ // <p 1>asdf|</p 7><p 7>|bye</p 12> + [<p>]hello</p><p>good[</p>] -> <p>asdfhello</p><p>goodbye</p>
967
+ //
968
+ // What about:
969
+ // <p 1>asdf|</p 7><p 7 op="inserted">|bye</p 12> + empty -> <p>asdfbye</p>
970
+ const endTokenDeleted = nodeEnd <= offsetTo;
971
+ // The start token deleted eg:
972
+ // |<p1 0>hey</p 6><p2 6>|asdf</p 12> + <p3>hello [</p>] -> <p3>hello asdf</p2>
973
+ // (del 0 7) + (ins <p>hello [</p>] openStart 0 openEnd 1)
974
+ // (<p1> pos 0) >= (from 0) && (nodeEnd 6) - 1 > (to 7) == false???
975
+ // (<p2> pos 6) >= (from 0) && (nodeEnd 12) - 1 > (to 7) == true
976
+ //
977
+ const startTokenDeleted = offsetPos >= offsetFrom; // && nodeEnd - 1 > offsetTo
978
+ if (node.isText ||
979
+ (!endTokenDeleted && startTokenDeleted) ||
980
+ (endTokenDeleted && !startTokenDeleted)) {
981
+ // Since we don't know which side to merge with wholly deleted TextNodes, we use this boolean to remember
982
+ // whether we have entered the endSide of the mergeable blockNodes. Also applies for partial TextNodes
983
+ // (which we could determine without this).
984
+ if (!endTokenDeleted && startTokenDeleted) {
985
+ mergingStartSide = false;
986
+ }
987
+ // Depth is often 1 when merging paragraphs or 2 for fully open blockquotes.
988
+ // Incase of merging text within a ReplaceAroundStep the depth might be 1
989
+ const depth = newTr.doc.resolve(offsetPos).depth;
990
+ const mergeContent = mergingStartSide
991
+ ? firstMergedNode === null || firstMergedNode === void 0 ? void 0 : firstMergedNode.mergedNodeContent
992
+ : lastMergedNode === null || lastMergedNode === void 0 ? void 0 : lastMergedNode.mergedNodeContent;
993
+ // Insert inside a merged node only if the slice was open (openStart > 0) and there exists mergedNodeContent.
994
+ // Then we only have to ensure the depth is at the right level, so say a fully open blockquote insert will
995
+ // be merged at the lowest, paragraph level, instead of blockquote level.
996
+ const mergeStartNode = endTokenDeleted && openStart > 0 && depth === openStart && mergeContent !== undefined;
997
+ // Same as above, merge nodes manually if there exists an open slice with mergeable content.
998
+ // Compared to deleting an end token however, the merged block node is set as deleted. This is due to
999
+ // ProseMirror node semantics as start tokens are considered to contain the actual node itself.
1000
+ const mergeEndNode = startTokenDeleted && openEnd > 0 && depth === openEnd && mergeContent !== undefined;
1001
+ 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
+ // Just as a fun fact that I found out while debugging this. Inserting text at paragraph position wraps
1015
+ // it into a new paragraph(!). So that's why you always offset your positions to insert it _inside_
1016
+ // the paragraph.
1017
+ if (mergeContent.size !== 0) {
1018
+ newTr.insert(insertPos, setFragmentAsInserted(mergeContent, createNewInsertAttrs(trackAttrs), schema));
1019
+ }
1020
+ // Okay this is a bit ridiculous but it's used to adjust the insert pos when track changes prevents deletions
1021
+ // of merged nodes & content, as just using mapped toA in that case isn't the same.
1022
+ // The calculation is a bit mysterious, I admit.
1023
+ // TODO delete/fix this?
1024
+ // 'should prevent replacing of blockquotes and break the slice into parts instead' test needs this
1025
+ // if (node.isText) {
1026
+ // mergedInsertPos = offsetPos - openEnd
1027
+ // }
1028
+ }
1029
+ else if (node.isText) {
1030
+ // Text deletion is handled even when the deletion doesn't completely wrap the text node
1031
+ // (which is basically the case most of the time)
1032
+ deleteTextIfInserted(node, offsetPos, newTr, schema, deleteAttrs, offsetFrom, offsetTo);
1033
+ }
1034
+ else ;
1035
+ }
1036
+ else if (nodeCompletelyDeleted) {
1037
+ deleteOrSetNodeDeleted(node, offsetPos, newTr, deleteAttrs);
1038
+ }
1039
+ }
1040
+ const newestStep = newTr.steps[newTr.steps.length - 1];
1041
+ if (step !== newestStep) {
1042
+ deleteMap.appendMap(newestStep.getMap());
1043
+ }
1044
+ });
1045
+ return {
1046
+ deleteMap,
1047
+ mergedInsertPos,
1048
+ newSliceContent: updatedSliceNodes
1049
+ ? prosemirrorModel.Fragment.fromArray(updatedSliceNodes)
1050
+ : insertSlice.content,
1051
+ };
1052
+ }
1053
+
1054
+ /**
1055
+ * Merges tracked marks between text nodes at a position
1056
+ *
1057
+ * Will work for any nodes that use tracked_insert or tracked_delete marks which may not be preferrable
1058
+ * if used for block nodes (since we possibly want to show the individual changed nodes).
1059
+ * Merging is done based on the userID, operation type and status.
1060
+ * @param pos
1061
+ * @param doc
1062
+ * @param newTr
1063
+ * @param schema
1064
+ */
1065
+ function mergeTrackedMarks(pos, doc, newTr, schema) {
1066
+ const resolved = doc.resolve(pos);
1067
+ const { nodeAfter, nodeBefore } = resolved;
1068
+ const leftMark = nodeBefore === null || nodeBefore === void 0 ? void 0 : nodeBefore.marks.filter((m) => m.type === schema.marks.tracked_insert || m.type === schema.marks.tracked_delete)[0];
1069
+ const rightMark = nodeAfter === null || nodeAfter === void 0 ? void 0 : nodeAfter.marks.filter((m) => m.type === schema.marks.tracked_insert || m.type === schema.marks.tracked_delete)[0];
1070
+ if (!nodeAfter || !nodeBefore || !leftMark || !rightMark || leftMark.type !== rightMark.type) {
1071
+ return;
1072
+ }
1073
+ const leftAttrs = leftMark.attrs;
1074
+ const rightAttrs = rightMark.attrs;
1075
+ if (!shouldMergeTrackedAttributes(leftAttrs.dataTracked, rightAttrs.dataTracked)) {
1076
+ return;
1077
+ }
1078
+ const newAttrs = {
1079
+ ...leftAttrs,
1080
+ createdAt: Math.max(leftAttrs.createdAt || 0, rightAttrs.createdAt || 0) || Date.now(),
1081
+ };
1082
+ const fromStartOfMark = pos - nodeBefore.nodeSize;
1083
+ const toEndOfMark = pos + nodeAfter.nodeSize;
1084
+ newTr.addMark(fromStartOfMark, toEndOfMark, leftMark.type.create(newAttrs));
1085
+ }
1086
+
1087
+ /*!
1088
+ * © 2021 Atypon Systems LLC
1089
+ *
1090
+ * Licensed under the Apache License, Version 2.0 (the "License");
1091
+ * you may not use this file except in compliance with the License.
1092
+ * You may obtain a copy of the License at
1093
+ *
1094
+ * http://www.apache.org/licenses/LICENSE-2.0
1095
+ *
1096
+ * Unless required by applicable law or agreed to in writing, software
1097
+ * distributed under the License is distributed on an "AS IS" BASIS,
1098
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1099
+ * See the License for the specific language governing permissions and
1100
+ * limitations under the License.
1101
+ */
1102
+ function trackReplaceAroundStep(step, oldState, newTr, attrs) {
1103
+ log.info('###### ReplaceAroundStep ######');
1104
+ // @ts-ignore
1105
+ const { from, to, gapFrom, gapTo, insert, slice, structure, } = step;
1106
+ if (from === gapFrom && to === gapTo) {
1107
+ log.info('WRAPPED IN SOMETHING');
1108
+ }
1109
+ else if (!slice.size || slice.content.content.length === 2) {
1110
+ log.info('UNWRAPPED FROM SOMETHING');
1111
+ }
1112
+ else if (slice.size === 2 && gapFrom - from === 1 && to - gapTo === 1) {
1113
+ log.info('REPLACED WRAPPING');
1114
+ }
1115
+ else {
1116
+ log.info('????');
1117
+ }
1118
+ if (gapFrom - from > to - gapTo) {
1119
+ log.info('DELETED BEFORE GAP FROM');
1120
+ }
1121
+ else if (gapFrom - from < to - gapTo) {
1122
+ log.info('DELETED AFTER GAP TO');
1123
+ }
1124
+ else {
1125
+ log.info('EQUAL REPLACE BETWEEN GAPS');
1126
+ }
1127
+ // Invert the transaction step to prevent it from actually deleting or inserting anything
1128
+ const newStep = step.invert(oldState.doc);
1129
+ const stepResult = newTr.maybeStep(newStep);
1130
+ if (stepResult.failed) {
1131
+ log.error(`inverting ReplaceAroundStep failed: "${stepResult.failed}"`, newStep);
1132
+ return;
1133
+ }
1134
+ const gap = oldState.doc.slice(gapFrom, gapTo);
1135
+ log.info('RETAINED GAP CONTENT', gap);
1136
+ // First apply the deleted range and update the insert slice to not include content that was deleted,
1137
+ // eg partial nodes in an open-ended slice
1138
+ const { deleteMap, newSliceContent } = deleteAndMergeSplitNodes(from, to, { start: gapFrom, end: gapTo }, newTr.doc, newTr, oldState.schema, attrs, slice);
1139
+ log.info('TR: new steps after applying delete', [...newTr.steps]);
1140
+ // We only want to insert when there something inside the gap (actually would this be always true?)
1141
+ // or insert slice wasn't just start/end tokens (which we already merged inside deleteAndMergeSplitBlockNodes)
1142
+ if (gap.size > 0 || (!structure && newSliceContent.size > 0)) {
1143
+ log.info('newSliceContent', newSliceContent);
1144
+ // Since deleteAndMergeSplitBlockNodes modified the slice to not to contain any merged nodes,
1145
+ // the sides should be equal. TODO can they be other than 0?
1146
+ const openStart = slice.openStart !== slice.openEnd || newSliceContent.size === 0 ? 0 : slice.openStart;
1147
+ const openEnd = slice.openStart !== slice.openEnd || newSliceContent.size === 0 ? 0 : slice.openEnd;
1148
+ let insertedSlice = new prosemirrorModel.Slice(setFragmentAsInserted(newSliceContent, createNewInsertAttrs(attrs), oldState.schema), openStart, openEnd);
1149
+ if (gap.size > 0) {
1150
+ log.info('insertedSlice before inserted gap', insertedSlice);
1151
+ insertedSlice = insertedSlice.insertAt(insertedSlice.size === 0 ? 0 : insert, gap.content);
1152
+ log.info('insertedSlice after inserted gap', insertedSlice);
1153
+ }
1154
+ const newStep = new prosemirrorTransform.ReplaceStep(deleteMap.map(gapFrom), deleteMap.map(gapTo), insertedSlice, false);
1155
+ const stepResult = newTr.maybeStep(newStep);
1156
+ if (stepResult.failed) {
1157
+ log.error(`insert ReplaceStep failed: "${stepResult.failed}"`, newStep);
1158
+ return;
1159
+ }
1160
+ log.info('new steps after applying insert', [...newTr.steps]);
1161
+ mergeTrackedMarks(deleteMap.map(gapFrom), newTr.doc, newTr, oldState.schema);
1162
+ mergeTrackedMarks(deleteMap.map(gapTo), newTr.doc, newTr, oldState.schema);
1163
+ }
1164
+ else {
1165
+ // Incase only deletion was applied, check whether tracked marks around deleted content can be merged
1166
+ mergeTrackedMarks(deleteMap.map(gapFrom), newTr.doc, newTr, oldState.schema);
1167
+ mergeTrackedMarks(deleteMap.map(gapTo), newTr.doc, newTr, oldState.schema);
1168
+ }
1169
+ }
1170
+
1171
+ /*!
1172
+ * © 2021 Atypon Systems LLC
1173
+ *
1174
+ * Licensed under the Apache License, Version 2.0 (the "License");
1175
+ * you may not use this file except in compliance with the License.
1176
+ * You may obtain a copy of the License at
1177
+ *
1178
+ * http://www.apache.org/licenses/LICENSE-2.0
1179
+ *
1180
+ * Unless required by applicable law or agreed to in writing, software
1181
+ * distributed under the License is distributed on an "AS IS" BASIS,
1182
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1183
+ * See the License for the specific language governing permissions and
1184
+ * limitations under the License.
1185
+ */
1186
+ function trackReplaceStep(step, oldState, newTr, attrs) {
1187
+ log.info('###### ReplaceStep ######');
1188
+ let selectionPos = 0;
1189
+ step.getMap().forEach((fromA, toA, fromB, toB) => {
1190
+ log.info(`changed ranges: ${fromA} ${toA} ${fromB} ${toB}`);
1191
+ const { slice } = step;
1192
+ // Invert the transaction step to prevent it from actually deleting or inserting anything
1193
+ const newStep = step.invert(oldState.doc);
1194
+ const stepResult = newTr.maybeStep(newStep);
1195
+ if (stepResult.failed) {
1196
+ log.error(`invert ReplaceStep failed: "${stepResult.failed}"`, newStep);
1197
+ return;
1198
+ }
1199
+ log.info('TR: steps before applying delete', [...newTr.steps]);
1200
+ // First apply the deleted range and update the insert slice to not include content that was deleted,
1201
+ // eg partial nodes in an open-ended slice
1202
+ const { deleteMap, mergedInsertPos, newSliceContent } = deleteAndMergeSplitNodes(fromA, toA, undefined, oldState.doc, newTr, oldState.schema, attrs, slice);
1203
+ log.info('TR: steps after applying delete', [...newTr.steps]);
1204
+ const adjustedInsertPos = mergedInsertPos !== null && mergedInsertPos !== void 0 ? mergedInsertPos : deleteMap.map(toA);
1205
+ if (newSliceContent.size > 0) {
1206
+ log.info('newSliceContent', newSliceContent);
1207
+ // Since deleteAndMergeSplitBlockNodes modified the slice to not to contain any merged nodes,
1208
+ // the sides should be equal. TODO can they be other than 0?
1209
+ const openStart = slice.openStart !== slice.openEnd ? 0 : slice.openStart;
1210
+ const openEnd = slice.openStart !== slice.openEnd ? 0 : slice.openEnd;
1211
+ const insertedSlice = new prosemirrorModel.Slice(setFragmentAsInserted(newSliceContent, createNewInsertAttrs(attrs), oldState.schema), openStart, openEnd);
1212
+ const newStep = new prosemirrorTransform.ReplaceStep(adjustedInsertPos, adjustedInsertPos, insertedSlice);
1213
+ const stepResult = newTr.maybeStep(newStep);
1214
+ if (stepResult.failed) {
1215
+ log.error(`insert ReplaceStep failed: "${stepResult.failed}"`, newStep);
1216
+ return;
1217
+ }
1218
+ log.info('new steps after applying insert', [...newTr.steps]);
1219
+ mergeTrackedMarks(adjustedInsertPos, newTr.doc, newTr, oldState.schema);
1220
+ mergeTrackedMarks(adjustedInsertPos + insertedSlice.size, newTr.doc, newTr, oldState.schema);
1221
+ selectionPos = adjustedInsertPos + insertedSlice.size;
1222
+ }
1223
+ else {
1224
+ // Incase only deletion was applied, check whether tracked marks around deleted content can be merged
1225
+ mergeTrackedMarks(adjustedInsertPos, newTr.doc, newTr, oldState.schema);
1226
+ selectionPos = fromA;
1227
+ }
1228
+ });
1229
+ return selectionPos;
1230
+ }
1231
+
1232
+ /**
1233
+ * Retrieves a static property from Selection class instead of having to use direct imports
1234
+ *
1235
+ * This skips the direct dependency to prosemirror-state where multiple versions might cause conflicts
1236
+ * as the created instances might belong to different prosemirror-state import than one used in the editor.
1237
+ * @param sel
1238
+ * @returns
1239
+ */
1240
+ const getSelectionStaticConstructor = (sel) => Object.getPrototypeOf(sel).constructor;
1241
+ /**
1242
+ * Inverts transactions to wrap their contents/operations with track data instead
1243
+ *
1244
+ * The main function of track changes that holds the most complex parts of this whole library.
1245
+ * Takes in as arguments the data from appendTransaction to reapply it with the track marks/attributes.
1246
+ * We could prevent the initial transaction from being applied all together but since invert works just
1247
+ * as well and we can use the intermediate doc for checking which nodes are changed, it's not prevented.
1248
+ *
1249
+ *
1250
+ * @param tr Original transaction
1251
+ * @param oldState State before transaction
1252
+ * @param newTr Transaction created from the new editor state
1253
+ * @param userID User id
1254
+ * @returns newTr that inverts the initial tr and applies track attributes/marks
1255
+ */
1256
+ function trackTransaction(tr, oldState, newTr, userID) {
1257
+ const emptyAttrs = {
1258
+ userID,
1259
+ createdAt: tr.time,
1260
+ status: exports.CHANGE_STATUS.pending,
1261
+ };
1262
+ // Must use constructor.name instead of instanceof as aliasing prosemirror-state is a lot more
1263
+ // difficult than prosemirror-transform
1264
+ const wasNodeSelection = tr.selection.constructor.name === 'NodeSelection';
1265
+ let iters = 0;
1266
+ log.info('ORIGINAL transaction', tr);
1267
+ tr.steps.forEach((step) => {
1268
+ log.info('transaction step', step);
1269
+ iters += 1;
1270
+ if (iters > 20) {
1271
+ console.error('@manuscripts/track-changes-plugin: Possible infinite loop in iterating tr.steps, tracking skipped!\n' +
1272
+ 'This is probably an error with the library, please report back to maintainers with a reproduction if possible', newTr);
1273
+ return;
1274
+ }
1275
+ else if (!(step instanceof prosemirrorTransform.ReplaceStep) && step.constructor.name === 'ReplaceStep') {
1276
+ console.error('@manuscripts/track-changes-plugin: Multiple prosemirror-transform packages imported, alias/dedupe them ' +
1277
+ 'or instanceof checks fail as well as creating new steps');
1278
+ return;
1279
+ }
1280
+ else if (step instanceof prosemirrorTransform.ReplaceStep) {
1281
+ const selectionPos = trackReplaceStep(step, oldState, newTr, emptyAttrs);
1282
+ if (!wasNodeSelection) {
1283
+ const sel = getSelectionStaticConstructor(tr.selection);
1284
+ // Use Selection.near to fix selections that point to a block node instead of inline content
1285
+ // eg when inserting a complete new paragraph. -1 finds the first valid position moving backwards
1286
+ // inside the content
1287
+ const near = sel.near(newTr.doc.resolve(selectionPos), -1);
1288
+ newTr.setSelection(near);
1289
+ }
1290
+ }
1291
+ else if (step instanceof prosemirrorTransform.ReplaceAroundStep) {
1292
+ trackReplaceAroundStep(step, oldState, newTr, emptyAttrs);
1293
+ // } else if (step instanceof AddMarkStep) {
1294
+ // } else if (step instanceof RemoveMarkStep) {
1295
+ }
1296
+ // TODO: here we could check whether adjacent inserts & deletes cancel each other out.
1297
+ // However, this should not be done by diffing and only matching node or char by char instead since
1298
+ // it's A easier and B more intuitive to user.
1299
+ // The old meta keys are not copied to the new transaction since this will cause race-conditions
1300
+ // when a single meta-field is expected to having been processed / removed. Generic input meta keys,
1301
+ // inputType and uiEvent, are re-added since some plugins might depend on them and process the transaction
1302
+ // after track-changes plugin.
1303
+ tr.getMeta('inputType') && newTr.setMeta('inputType', tr.getMeta('inputType'));
1304
+ tr.getMeta('uiEvent') && newTr.setMeta('uiEvent', tr.getMeta('uiEvent'));
1305
+ });
1306
+ // This is kinda hacky solution at the moment to maintain NodeSelections over transactions
1307
+ // These are required by at least cross-references and links to activate their selector pop-ups
1308
+ if (wasNodeSelection) {
1309
+ // And -1 here is necessary to keep the selection pointing at the start of the node
1310
+ // (or something, breaks with cross-references otherwise)
1311
+ const mappedPos = newTr.mapping.map(tr.selection.from, -1);
1312
+ const sel = getSelectionStaticConstructor(tr.selection);
1313
+ newTr.setSelection(sel.create(newTr.doc, mappedPos));
1314
+ }
1315
+ log.info('NEW transaction', newTr);
1316
+ return newTr;
1317
+ }
1318
+
1319
+ exports.TrackChangesStatus = void 0;
1320
+ (function (TrackChangesStatus) {
1321
+ TrackChangesStatus["enabled"] = "enabled";
1322
+ TrackChangesStatus["viewSnapshots"] = "view-snapshots";
1323
+ TrackChangesStatus["disabled"] = "disabled";
1324
+ })(exports.TrackChangesStatus || (exports.TrackChangesStatus = {}));
1325
+
1326
+ /*!
1327
+ * © 2021 Atypon Systems LLC
1328
+ *
1329
+ * Licensed under the Apache License, Version 2.0 (the "License");
1330
+ * you may not use this file except in compliance with the License.
1331
+ * You may obtain a copy of the License at
1332
+ *
1333
+ * http://www.apache.org/licenses/LICENSE-2.0
1334
+ *
1335
+ * Unless required by applicable law or agreed to in writing, software
1336
+ * distributed under the License is distributed on an "AS IS" BASIS,
1337
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1338
+ * See the License for the specific language governing permissions and
1339
+ * limitations under the License.
1340
+ */
1341
+ const trackChangesPluginKey = new prosemirrorState.PluginKey('track-changes');
1342
+ /**
1343
+ * The ProseMirror plugin needed to enable track-changes.
1344
+ *
1345
+ * Accepts an empty options object as an argument but note that this uses 'anonymous:Anonymous' as the default userID.
1346
+ * @param opts
1347
+ */
1348
+ const trackChangesPlugin = (opts = { userID: 'anonymous:Anonymous' }) => {
1349
+ const { userID, debug, skipTrsWithMetas = [] } = opts;
1350
+ let editorView;
1351
+ if (debug) {
1352
+ enableDebug(true);
1353
+ }
1354
+ return new prosemirrorState.Plugin({
1355
+ key: trackChangesPluginKey,
1356
+ props: {
1357
+ editable(state) {
1358
+ var _a;
1359
+ return ((_a = trackChangesPluginKey.getState(state)) === null || _a === void 0 ? void 0 : _a.status) !== exports.TrackChangesStatus.viewSnapshots;
1360
+ },
1361
+ },
1362
+ state: {
1363
+ init(_config, state) {
1364
+ return {
1365
+ status: exports.TrackChangesStatus.enabled,
1366
+ userID,
1367
+ changeSet: findChanges(state),
1368
+ };
1369
+ },
1370
+ apply(tr, pluginState, _oldState, newState) {
1371
+ const setUserID = getAction(tr, exports.TrackChangesAction.setUserID);
1372
+ const setStatus = getAction(tr, exports.TrackChangesAction.setPluginStatus);
1373
+ if (setUserID) {
1374
+ return { ...pluginState, userID: setUserID };
1375
+ }
1376
+ else if (setStatus) {
1377
+ return {
1378
+ ...pluginState,
1379
+ status: setStatus,
1380
+ changeSet: findChanges(newState),
1381
+ };
1382
+ }
1383
+ else if (pluginState.status === exports.TrackChangesStatus.disabled) {
1384
+ return { ...pluginState, changeSet: new ChangeSet() };
1385
+ }
1386
+ let { changeSet, ...rest } = pluginState;
1387
+ const updatedChangeIds = getAction(tr, exports.TrackChangesAction.updateChanges);
1388
+ if (updatedChangeIds || getAction(tr, exports.TrackChangesAction.refreshChanges)) {
1389
+ changeSet = findChanges(newState);
1390
+ }
1391
+ return {
1392
+ changeSet,
1393
+ ...rest,
1394
+ };
1395
+ },
1396
+ },
1397
+ view(p) {
1398
+ editorView = p;
1399
+ return {
1400
+ update: undefined,
1401
+ destroy: undefined,
1402
+ };
1403
+ },
1404
+ appendTransaction(trs, oldState, newState) {
1405
+ const pluginState = trackChangesPluginKey.getState(newState);
1406
+ if (!pluginState ||
1407
+ pluginState.status === exports.TrackChangesStatus.disabled ||
1408
+ !(editorView === null || editorView === void 0 ? void 0 : editorView.editable)) {
1409
+ return null;
1410
+ }
1411
+ const { userID, changeSet } = pluginState;
1412
+ let createdTr = newState.tr, docChanged = false;
1413
+ log.info('TRS', trs);
1414
+ trs.forEach((tr) => {
1415
+ const wasAppended = tr.getMeta('appendedTransaction');
1416
+ const skipMetaUsed = skipTrsWithMetas.some((m) => tr.getMeta(m) || (wasAppended === null || wasAppended === void 0 ? void 0 : wasAppended.getMeta(m)));
1417
+ const skipTrackUsed = getAction(tr, exports.TrackChangesAction.skipTrack) ||
1418
+ (wasAppended && getAction(wasAppended, exports.TrackChangesAction.skipTrack));
1419
+ if (tr.docChanged && !skipMetaUsed && !skipTrackUsed && !tr.getMeta('history$')) {
1420
+ createdTr = trackTransaction(tr, oldState, createdTr, userID);
1421
+ }
1422
+ docChanged = docChanged || tr.docChanged;
1423
+ const setChangeStatuses = getAction(tr, exports.TrackChangesAction.setChangeStatuses);
1424
+ if (setChangeStatuses) {
1425
+ const { status, ids } = setChangeStatuses;
1426
+ ids.forEach((changeId) => {
1427
+ const change = changeSet === null || changeSet === void 0 ? void 0 : changeSet.get(changeId);
1428
+ if (change) {
1429
+ createdTr = updateChangeAttrs(createdTr, change, { status }, oldState.schema);
1430
+ setAction(createdTr, exports.TrackChangesAction.updateChanges, [change.id]);
1431
+ }
1432
+ });
1433
+ }
1434
+ else if (getAction(tr, exports.TrackChangesAction.applyAndRemoveChanges)) {
1435
+ const mapping = applyAcceptedRejectedChanges(createdTr, oldState.schema, changeSet.nodeChanges);
1436
+ applyAcceptedRejectedChanges(createdTr, oldState.schema, changeSet.textChanges, mapping);
1437
+ setAction(createdTr, exports.TrackChangesAction.refreshChanges, true);
1438
+ }
1439
+ });
1440
+ const changed = pluginState.changeSet.hasInconsistentData &&
1441
+ fixInconsistentChanges(pluginState.changeSet, userID, createdTr, oldState.schema);
1442
+ if (changed) {
1443
+ log.warn('had to fix inconsistent changes in', createdTr);
1444
+ }
1445
+ if (docChanged || createdTr.docChanged || changed) {
1446
+ createdTr.setMeta('origin', trackChangesPluginKey);
1447
+ return setAction(createdTr, exports.TrackChangesAction.refreshChanges, true);
1448
+ }
1449
+ return null;
1450
+ },
1451
+ });
1452
+ };
1453
+
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
+ /**
1470
+ * Sets track-changes plugin's status to any of: 'enabled' 'disabled' 'viewSnapshots'. Passing undefined will
1471
+ * set 'enabled' status to 'disabled' and 'disabled' | 'viewSnapshots' status to 'enabled'.
1472
+ *
1473
+ * In disabled view, the plugin is completely inactive and changes are not updated anymore.
1474
+ * In viewSnasphots state, editor is set uneditable by editable prop that allows only selection changes
1475
+ * to the document.
1476
+ * @param status
1477
+ */
1478
+ const setTrackingStatus = (status) => (state, dispatch) => {
1479
+ var _a;
1480
+ const currentStatus = (_a = trackChangesPluginKey.getState(state)) === null || _a === void 0 ? void 0 : _a.status;
1481
+ if (currentStatus) {
1482
+ let newStatus = status;
1483
+ if (newStatus === undefined) {
1484
+ newStatus =
1485
+ currentStatus === exports.TrackChangesStatus.enabled
1486
+ ? exports.TrackChangesStatus.disabled
1487
+ : exports.TrackChangesStatus.enabled;
1488
+ }
1489
+ dispatch && dispatch(setAction(state.tr, exports.TrackChangesAction.setPluginStatus, newStatus));
1490
+ return true;
1491
+ }
1492
+ return false;
1493
+ };
1494
+ /**
1495
+ * Appends a transaction to set change attributes/marks' statuses to any of: 'pending' 'accepted' 'rejected'.
1496
+ * @param status
1497
+ * @param ids
1498
+ */
1499
+ const setChangeStatuses = (status, ids) => (state, dispatch) => {
1500
+ dispatch &&
1501
+ dispatch(setAction(state.tr, exports.TrackChangesAction.setChangeStatuses, {
1502
+ status,
1503
+ ids,
1504
+ }));
1505
+ return true;
1506
+ };
1507
+ /**
1508
+ * Sets track-changes plugin's userID.
1509
+ * @param userID
1510
+ */
1511
+ const setUserID = (userID) => (state, dispatch) => {
1512
+ dispatch && dispatch(setAction(state.tr, exports.TrackChangesAction.setUserID, userID));
1513
+ return true;
1514
+ };
1515
+ /**
1516
+ * Appends a transaction that applies all 'accepted' and 'rejected' changes to the document.
1517
+ */
1518
+ const applyAndRemoveChanges = () => (state, dispatch) => {
1519
+ dispatch && dispatch(setAction(state.tr, exports.TrackChangesAction.applyAndRemoveChanges, true));
1520
+ return true;
1521
+ };
1522
+ /**
1523
+ * Runs `findChanges` to iterate over the document to collect changes into a new ChangeSet.
1524
+ */
1525
+ const refreshChanges = () => (state, dispatch) => {
1526
+ dispatch && dispatch(setAction(state.tr, exports.TrackChangesAction.updateChanges, []));
1527
+ return true;
1528
+ };
1529
+ /**
1530
+ * Adds track attributes for a block node. For testing puroses
1531
+ */
1532
+ const setParagraphTestAttribute = (val = 'changed') => (state, dispatch) => {
1533
+ var _a;
1534
+ const cursor = state.selection.head;
1535
+ const blockNodePos = state.doc.resolve(cursor).start(1) - 1;
1536
+ if (((_a = state.doc.resolve(blockNodePos).nodeAfter) === null || _a === void 0 ? void 0 : _a.type) === state.schema.nodes.paragraph &&
1537
+ dispatch) {
1538
+ dispatch(state.tr.setNodeMarkup(blockNodePos, undefined, {
1539
+ testAttribute: val,
1540
+ }));
1541
+ return true;
1542
+ }
1543
+ return false;
1544
+ };
1545
+
1546
+ var commands = /*#__PURE__*/Object.freeze({
1547
+ __proto__: null,
1548
+ setTrackingStatus: setTrackingStatus,
1549
+ setChangeStatuses: setChangeStatuses,
1550
+ setUserID: setUserID,
1551
+ applyAndRemoveChanges: applyAndRemoveChanges,
1552
+ refreshChanges: refreshChanges,
1553
+ setParagraphTestAttribute: setParagraphTestAttribute
1554
+ });
1555
+
1556
+ exports.ChangeSet = ChangeSet;
1557
+ exports.enableDebug = enableDebug;
1558
+ exports.getAction = getAction;
1559
+ exports.setAction = setAction;
1560
+ exports.trackChangesPlugin = trackChangesPlugin;
1561
+ exports.trackChangesPluginKey = trackChangesPluginKey;
1562
+ exports.trackCommands = commands;