@manuscripts/track-changes-plugin 1.7.2 → 1.7.3
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/cjs/ChangeSet.js +155 -0
- package/dist/cjs/actions.js +23 -0
- package/dist/cjs/change-steps/diffChangeSteps.js +63 -0
- package/dist/cjs/change-steps/matchInserted.js +70 -0
- package/dist/cjs/change-steps/processChangeSteps.js +141 -0
- package/dist/cjs/changes/applyChanges.js +81 -0
- package/dist/cjs/changes/findChanges.js +72 -0
- package/dist/cjs/changes/fixInconsistentChanges.js +21 -0
- package/dist/cjs/changes/updateChangeAttrs.js +65 -0
- package/dist/cjs/commands.js +47 -0
- package/dist/cjs/compute/nodeHelpers.js +77 -0
- package/dist/cjs/compute/setFragmentAsInserted.js +53 -0
- package/dist/cjs/compute/splitSliceIntoMergedParts.js +57 -0
- package/dist/cjs/index.js +41 -0
- package/dist/cjs/mutate/deleteAndMergeSplitNodes.js +105 -0
- package/dist/cjs/mutate/deleteNode.js +43 -0
- package/dist/cjs/mutate/deleteText.js +29 -0
- package/dist/cjs/mutate/mergeNode.js +21 -0
- package/dist/cjs/mutate/mergeTrackedMarks.js +25 -0
- package/dist/cjs/plugin.js +118 -0
- package/dist/cjs/steps/trackReplaceAroundStep.js +72 -0
- package/dist/cjs/steps/trackReplaceStep.js +79 -0
- package/dist/cjs/steps/trackTransaction.js +88 -0
- package/dist/cjs/types/change.js +15 -0
- package/dist/cjs/types/pm.js +2 -0
- package/dist/cjs/types/step.js +2 -0
- package/dist/cjs/types/track.js +9 -0
- package/dist/cjs/utils/logger.js +43 -0
- package/dist/cjs/utils/track-utils.js +28 -0
- package/dist/cjs/utils/uuidv4.js +10 -0
- package/dist/es/ChangeSet.js +151 -0
- package/dist/es/actions.js +17 -0
- package/dist/es/change-steps/diffChangeSteps.js +59 -0
- package/dist/es/change-steps/matchInserted.js +66 -0
- package/dist/es/change-steps/processChangeSteps.js +114 -0
- package/dist/es/changes/applyChanges.js +77 -0
- package/dist/es/changes/findChanges.js +68 -0
- package/dist/es/changes/fixInconsistentChanges.js +17 -0
- package/dist/es/changes/updateChangeAttrs.js +60 -0
- package/dist/es/commands.js +39 -0
- package/dist/es/compute/nodeHelpers.js +67 -0
- package/dist/es/compute/setFragmentAsInserted.js +49 -0
- package/dist/es/compute/splitSliceIntoMergedParts.js +53 -0
- package/dist/es/index.js +7 -0
- package/dist/es/mutate/deleteAndMergeSplitNodes.js +78 -0
- package/dist/es/mutate/deleteNode.js +38 -0
- package/dist/es/mutate/deleteText.js +25 -0
- package/dist/es/mutate/mergeNode.js +17 -0
- package/dist/es/mutate/mergeTrackedMarks.js +21 -0
- package/dist/es/plugin.js +114 -0
- package/dist/es/steps/trackReplaceAroundStep.js +45 -0
- package/dist/es/steps/trackReplaceStep.js +52 -0
- package/dist/es/steps/trackTransaction.js +84 -0
- package/dist/es/types/change.js +12 -0
- package/dist/es/types/pm.js +1 -0
- package/dist/es/types/step.js +1 -0
- package/dist/es/types/track.js +6 -0
- package/dist/es/utils/logger.js +36 -0
- package/dist/es/utils/track-utils.js +22 -0
- package/dist/es/utils/uuidv4.js +6 -0
- package/dist/types/ChangeSet.d.ts +28 -0
- package/dist/{actions.d.ts → types/actions.d.ts} +27 -60
- package/dist/{change-steps → types/change-steps}/diffChangeSteps.d.ts +2 -2
- package/dist/types/change-steps/matchInserted.d.ts +3 -0
- package/dist/types/change-steps/processChangeSteps.d.ts +6 -0
- package/dist/types/changes/applyChanges.d.ts +5 -0
- package/dist/types/changes/findChanges.d.ts +3 -0
- package/dist/types/changes/fixInconsistentChanges.d.ts +4 -0
- package/dist/types/changes/updateChangeAttrs.d.ts +6 -0
- package/dist/types/commands.d.ts +8 -0
- package/dist/{compute → types/compute}/nodeHelpers.d.ts +13 -28
- package/dist/types/compute/setFragmentAsInserted.d.ts +3 -0
- package/dist/types/compute/splitSliceIntoMergedParts.d.ts +13 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/mutate/deleteAndMergeSplitNodes.d.ts +13 -0
- package/dist/types/mutate/deleteNode.d.ts +5 -0
- package/dist/types/mutate/deleteText.d.ts +4 -0
- package/dist/types/mutate/mergeNode.d.ts +3 -0
- package/dist/types/mutate/mergeTrackedMarks.d.ts +3 -0
- package/dist/types/plugin.d.ts +4 -0
- package/dist/{steps → types/steps}/trackReplaceAroundStep.d.ts +5 -5
- package/dist/types/steps/trackReplaceStep.d.ts +6 -0
- package/dist/types/steps/trackTransaction.d.ts +2 -0
- package/dist/types/types/change.d.ts +61 -0
- package/dist/types/types/pm.d.ts +12 -0
- package/dist/types/{step.d.ts → types/step.d.ts} +38 -53
- package/dist/types/types/track.d.ts +30 -0
- package/dist/types/utils/logger.d.ts +8 -0
- package/dist/{utils → types/utils}/track-utils.d.ts +4 -4
- package/dist/types/utils/uuidv4.d.ts +1 -0
- package/package.json +18 -39
- package/LICENSE +0 -201
- package/dist/ChangeSet.d.ts +0 -71
- package/dist/change-steps/matchInserted.d.ts +0 -13
- package/dist/change-steps/processChangeSteps.d.ts +0 -21
- package/dist/changes/applyChanges.d.ts +0 -28
- package/dist/changes/findChanges.d.ts +0 -27
- package/dist/changes/fixInconsistentChanges.d.ts +0 -29
- package/dist/changes/updateChangeAttrs.d.ts +0 -21
- package/dist/commands.d.ts +0 -47
- package/dist/compute/setFragmentAsInserted.d.ts +0 -18
- package/dist/compute/splitSliceIntoMergedParts.d.ts +0 -41
- package/dist/index.cjs +0 -2026
- package/dist/index.d.ts +0 -22
- package/dist/index.js +0 -2013
- package/dist/mutate/deleteAndMergeSplitNodes.d.ts +0 -53
- package/dist/mutate/deleteNode.d.ts +0 -36
- package/dist/mutate/deleteText.d.ts +0 -33
- package/dist/mutate/mergeNode.d.ts +0 -25
- package/dist/mutate/mergeTrackedMarks.d.ts +0 -29
- package/dist/plugin.d.ts +0 -25
- package/dist/steps/trackReplaceStep.d.ts +0 -21
- package/dist/steps/trackTransaction.d.ts +0 -17
- package/dist/types/change.d.ts +0 -76
- package/dist/types/pm.d.ts +0 -27
- package/dist/types/track.d.ts +0 -45
- package/dist/utils/logger.d.ts +0 -27
- package/dist/utils/uuidv4.d.ts +0 -16
package/dist/index.cjs
DELETED
|
@@ -1,2026 +0,0 @@
|
|
|
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
|
-
var TrackChangesAction;
|
|
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["refreshChanges"] = "track-changes-refresh-changes";
|
|
21
|
-
TrackChangesAction["applyAndRemoveChanges"] = "track-changes-apply-remove-changes";
|
|
22
|
-
TrackChangesAction["updateMetaNode"] = "track-changes-update-meta-node";
|
|
23
|
-
})(TrackChangesAction || (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
|
-
* 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);
|
|
49
|
-
|
|
50
|
-
/******************************************************************************
|
|
51
|
-
Copyright (c) Microsoft Corporation.
|
|
52
|
-
|
|
53
|
-
Permission to use, copy, modify, and/or distribute this software for any
|
|
54
|
-
purpose with or without fee is hereby granted.
|
|
55
|
-
|
|
56
|
-
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
57
|
-
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
58
|
-
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
59
|
-
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
60
|
-
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
61
|
-
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
62
|
-
PERFORMANCE OF THIS SOFTWARE.
|
|
63
|
-
***************************************************************************** */
|
|
64
|
-
|
|
65
|
-
function __classPrivateFieldGet(receiver, state, kind, f) {
|
|
66
|
-
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
67
|
-
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");
|
|
68
|
-
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function __classPrivateFieldSet(receiver, state, value, kind, f) {
|
|
72
|
-
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
73
|
-
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
74
|
-
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");
|
|
75
|
-
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/*!
|
|
79
|
-
* © 2021 Atypon Systems LLC
|
|
80
|
-
*
|
|
81
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
82
|
-
* you may not use this file except in compliance with the License.
|
|
83
|
-
* You may obtain a copy of the License at
|
|
84
|
-
*
|
|
85
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
86
|
-
*
|
|
87
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
88
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
89
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
90
|
-
* See the License for the specific language governing permissions and
|
|
91
|
-
* limitations under the License.
|
|
92
|
-
*/
|
|
93
|
-
exports.CHANGE_OPERATION = void 0;
|
|
94
|
-
(function (CHANGE_OPERATION) {
|
|
95
|
-
CHANGE_OPERATION["insert"] = "insert";
|
|
96
|
-
CHANGE_OPERATION["delete"] = "delete";
|
|
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;
|
|
104
|
-
(function (CHANGE_STATUS) {
|
|
105
|
-
CHANGE_STATUS["accepted"] = "accepted";
|
|
106
|
-
CHANGE_STATUS["rejected"] = "rejected";
|
|
107
|
-
CHANGE_STATUS["pending"] = "pending";
|
|
108
|
-
})(exports.CHANGE_STATUS || (exports.CHANGE_STATUS = {}));
|
|
109
|
-
|
|
110
|
-
/*!
|
|
111
|
-
* © 2021 Atypon Systems LLC
|
|
112
|
-
*
|
|
113
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
114
|
-
* you may not use this file except in compliance with the License.
|
|
115
|
-
* You may obtain a copy of the License at
|
|
116
|
-
*
|
|
117
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
118
|
-
*
|
|
119
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
120
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
121
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
122
|
-
* See the License for the specific language governing permissions and
|
|
123
|
-
* limitations under the License.
|
|
124
|
-
*/
|
|
125
|
-
const logger = debug__default["default"]('track');
|
|
126
|
-
const log = {
|
|
127
|
-
info(str, obj) {
|
|
128
|
-
if (obj) {
|
|
129
|
-
logger(str, obj);
|
|
130
|
-
}
|
|
131
|
-
else {
|
|
132
|
-
logger(str);
|
|
133
|
-
}
|
|
134
|
-
},
|
|
135
|
-
warn(str, obj) {
|
|
136
|
-
if (obj) {
|
|
137
|
-
logger(`%c WARNING ${str}`, 'color: #f3f32c', obj);
|
|
138
|
-
}
|
|
139
|
-
else {
|
|
140
|
-
logger(`%c WARNING ${str}`, 'color: #f3f32c');
|
|
141
|
-
}
|
|
142
|
-
},
|
|
143
|
-
error(str, obj) {
|
|
144
|
-
if (obj) {
|
|
145
|
-
logger(`%c ERROR ${str}`, 'color: #ff4242', obj);
|
|
146
|
-
}
|
|
147
|
-
else {
|
|
148
|
-
logger(`%c ERROR ${str}`, 'color: #ff4242');
|
|
149
|
-
}
|
|
150
|
-
},
|
|
151
|
-
};
|
|
152
|
-
/**
|
|
153
|
-
* Sets debug logging enabled/disabled.
|
|
154
|
-
* @param enabled
|
|
155
|
-
*/
|
|
156
|
-
const enableDebug = (enabled) => {
|
|
157
|
-
if (enabled) {
|
|
158
|
-
debug__default["default"].enable('track');
|
|
159
|
-
}
|
|
160
|
-
else {
|
|
161
|
-
debug__default["default"].disable();
|
|
162
|
-
}
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
var _ChangeSet_instances, _ChangeSet_changes, _ChangeSet_isSameNodeChange, _ChangeSet_isNotPendingOrDeleted;
|
|
166
|
-
/**
|
|
167
|
-
* ChangeSet is a data structure to contain the tracked changes with some utility methods and computed
|
|
168
|
-
* values to allow easier operability.
|
|
169
|
-
*/
|
|
170
|
-
class ChangeSet {
|
|
171
|
-
constructor(changes = []) {
|
|
172
|
-
_ChangeSet_instances.add(this);
|
|
173
|
-
_ChangeSet_changes.set(this, void 0);
|
|
174
|
-
__classPrivateFieldSet(this, _ChangeSet_changes, changes, "f");
|
|
175
|
-
}
|
|
176
|
-
/**
|
|
177
|
-
* List of all the valid TrackedChanges. This prevents for example changes with duplicate ids being shown
|
|
178
|
-
* in the UI, causing errors.
|
|
179
|
-
*/
|
|
180
|
-
get changes() {
|
|
181
|
-
const iteratedIds = new Set();
|
|
182
|
-
return __classPrivateFieldGet(this, _ChangeSet_changes, "f").filter((c) => {
|
|
183
|
-
const valid = !iteratedIds.has(c.dataTracked.id) && ChangeSet.isValidDataTracked(c.dataTracked);
|
|
184
|
-
iteratedIds.add(c.dataTracked.id);
|
|
185
|
-
return valid;
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
get invalidChanges() {
|
|
189
|
-
return __classPrivateFieldGet(this, _ChangeSet_changes, "f").filter((c) => !this.changes.find((cc) => c.id === cc.id));
|
|
190
|
-
}
|
|
191
|
-
/**
|
|
192
|
-
* List of 1-level nested changes where the top-most node change contains all the changes within its start
|
|
193
|
-
* and end position. This is useful for showing the changes as groups in the UI.
|
|
194
|
-
*/
|
|
195
|
-
get changeTree() {
|
|
196
|
-
const rootNodes = [];
|
|
197
|
-
let currentNodeChange;
|
|
198
|
-
this.changes.forEach((c) => {
|
|
199
|
-
if (currentNodeChange && c.from >= currentNodeChange.to) {
|
|
200
|
-
rootNodes.push(currentNodeChange);
|
|
201
|
-
currentNodeChange = undefined;
|
|
202
|
-
}
|
|
203
|
-
if (currentNodeChange &&
|
|
204
|
-
c.from < currentNodeChange.to &&
|
|
205
|
-
!(__classPrivateFieldGet(this, _ChangeSet_instances, "m", _ChangeSet_isSameNodeChange).call(this, currentNodeChange, c) &&
|
|
206
|
-
__classPrivateFieldGet(this, _ChangeSet_instances, "m", _ChangeSet_isNotPendingOrDeleted).call(this, currentNodeChange))) {
|
|
207
|
-
currentNodeChange.children.push(c);
|
|
208
|
-
}
|
|
209
|
-
else if (c.type === 'node-change') {
|
|
210
|
-
currentNodeChange = { ...c, children: [] };
|
|
211
|
-
}
|
|
212
|
-
else {
|
|
213
|
-
rootNodes.push(c);
|
|
214
|
-
}
|
|
215
|
-
});
|
|
216
|
-
if (currentNodeChange) {
|
|
217
|
-
rootNodes.push(currentNodeChange);
|
|
218
|
-
}
|
|
219
|
-
return rootNodes;
|
|
220
|
-
}
|
|
221
|
-
get pending() {
|
|
222
|
-
return this.changeTree.filter((c) => c.dataTracked.status === exports.CHANGE_STATUS.pending);
|
|
223
|
-
}
|
|
224
|
-
get accepted() {
|
|
225
|
-
return this.changeTree.filter((c) => c.dataTracked.status === exports.CHANGE_STATUS.accepted);
|
|
226
|
-
}
|
|
227
|
-
get rejected() {
|
|
228
|
-
return this.changeTree.filter((c) => c.dataTracked.status === exports.CHANGE_STATUS.rejected);
|
|
229
|
-
}
|
|
230
|
-
get textChanges() {
|
|
231
|
-
return this.changes.filter((c) => c.type === 'text-change');
|
|
232
|
-
}
|
|
233
|
-
get nodeChanges() {
|
|
234
|
-
return this.changes.filter((c) => c.type === 'node-change');
|
|
235
|
-
}
|
|
236
|
-
get nodeAttrChanges() {
|
|
237
|
-
return this.changes.filter((c) => c.type === 'node-attr-change');
|
|
238
|
-
}
|
|
239
|
-
get bothNodeChanges() {
|
|
240
|
-
return this.changes.filter((c) => c.type === 'node-change' || c.type === 'node-attr-change');
|
|
241
|
-
}
|
|
242
|
-
get isEmpty() {
|
|
243
|
-
return __classPrivateFieldGet(this, _ChangeSet_changes, "f").length === 0;
|
|
244
|
-
}
|
|
245
|
-
/**
|
|
246
|
-
* Used to determine whether `fixInconsistentChanges` has to be executed to replace eg duplicate ids or
|
|
247
|
-
* changes that are missing attributes.
|
|
248
|
-
*/
|
|
249
|
-
get hasInconsistentData() {
|
|
250
|
-
return this.hasDuplicateIds || this.hasIncompleteAttrs;
|
|
251
|
-
}
|
|
252
|
-
get hasDuplicateIds() {
|
|
253
|
-
const iterated = new Set();
|
|
254
|
-
return __classPrivateFieldGet(this, _ChangeSet_changes, "f").some((c) => {
|
|
255
|
-
if (iterated.has(c.id)) {
|
|
256
|
-
return true;
|
|
257
|
-
}
|
|
258
|
-
iterated.add(c.id);
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
get hasIncompleteAttrs() {
|
|
262
|
-
return __classPrivateFieldGet(this, _ChangeSet_changes, "f").some((c) => !ChangeSet.isValidDataTracked(c.dataTracked));
|
|
263
|
-
}
|
|
264
|
-
get(id) {
|
|
265
|
-
return __classPrivateFieldGet(this, _ChangeSet_changes, "f").find((c) => c.id === id);
|
|
266
|
-
}
|
|
267
|
-
getIn(ids) {
|
|
268
|
-
return ids
|
|
269
|
-
.map((id) => __classPrivateFieldGet(this, _ChangeSet_changes, "f").find((c) => c.id === id))
|
|
270
|
-
.filter((c) => c !== undefined);
|
|
271
|
-
}
|
|
272
|
-
getNotIn(ids) {
|
|
273
|
-
return __classPrivateFieldGet(this, _ChangeSet_changes, "f").filter((c) => ids.includes(c.id));
|
|
274
|
-
}
|
|
275
|
-
/**
|
|
276
|
-
* Flattens a changeTree into a list of IDs
|
|
277
|
-
* @param changes
|
|
278
|
-
*/
|
|
279
|
-
static flattenTreeToIds(changes) {
|
|
280
|
-
return changes.flatMap((c) => this.isNodeChange(c) ? [c.id, ...c.children.map((c) => c.id)] : c.id);
|
|
281
|
-
}
|
|
282
|
-
/**
|
|
283
|
-
* Determines whether a change should be deleted when applying it to the document.
|
|
284
|
-
* @param change
|
|
285
|
-
*/
|
|
286
|
-
static shouldDeleteChange(change) {
|
|
287
|
-
const { status, operation } = change.dataTracked;
|
|
288
|
-
return ((operation === exports.CHANGE_OPERATION.insert && status === exports.CHANGE_STATUS.rejected) ||
|
|
289
|
-
(operation === exports.CHANGE_OPERATION.delete && status === exports.CHANGE_STATUS.accepted));
|
|
290
|
-
}
|
|
291
|
-
/**
|
|
292
|
-
* Checks whether change attributes contain all TrackedAttrs keys with non-undefined values
|
|
293
|
-
* @param dataTracked
|
|
294
|
-
*/
|
|
295
|
-
static isValidDataTracked(dataTracked = {}) {
|
|
296
|
-
if ('dataTracked' in dataTracked) {
|
|
297
|
-
log.warn('passed "dataTracked" as property to isValidTrackedAttrs()', dataTracked);
|
|
298
|
-
}
|
|
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(dataTracked).filter(([key, val]) => trackedKeys.includes(key));
|
|
311
|
-
const optionalEntries = Object.entries(dataTracked).filter(([key, val]) => optionalKeys.includes(key));
|
|
312
|
-
return (entries.length === trackedKeys.length &&
|
|
313
|
-
entries.every(([key, val]) => trackedKeys.includes(key) && val !== undefined) &&
|
|
314
|
-
optionalEntries.every(([key, val]) => optionalKeys.includes(key) && val !== undefined) &&
|
|
315
|
-
(dataTracked.id || '').length > 0 // Changes created with undefined id have '' as placeholder
|
|
316
|
-
);
|
|
317
|
-
}
|
|
318
|
-
static isTextChange(change) {
|
|
319
|
-
return change.type === 'text-change';
|
|
320
|
-
}
|
|
321
|
-
static isNodeChange(change) {
|
|
322
|
-
return change.type === 'node-change';
|
|
323
|
-
}
|
|
324
|
-
static isNodeAttrChange(change) {
|
|
325
|
-
return change.type === 'node-attr-change';
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
_ChangeSet_changes = new WeakMap(), _ChangeSet_instances = new WeakSet(), _ChangeSet_isSameNodeChange = function _ChangeSet_isSameNodeChange(currentChange, nextChange) {
|
|
329
|
-
return currentChange.from === nextChange.from && currentChange.to === nextChange.to;
|
|
330
|
-
}, _ChangeSet_isNotPendingOrDeleted = function _ChangeSet_isNotPendingOrDeleted(change) {
|
|
331
|
-
return (change.dataTracked.operation !== exports.CHANGE_OPERATION.delete &&
|
|
332
|
-
change.dataTracked.status !== exports.CHANGE_STATUS.pending);
|
|
333
|
-
};
|
|
334
|
-
|
|
335
|
-
/*!
|
|
336
|
-
* © 2021 Atypon Systems LLC
|
|
337
|
-
*
|
|
338
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
339
|
-
* you may not use this file except in compliance with the License.
|
|
340
|
-
* You may obtain a copy of the License at
|
|
341
|
-
*
|
|
342
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
343
|
-
*
|
|
344
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
345
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
346
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
347
|
-
* See the License for the specific language governing permissions and
|
|
348
|
-
* limitations under the License.
|
|
349
|
-
*/
|
|
350
|
-
function uuidv4() {
|
|
351
|
-
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
|
352
|
-
const r = (Math.random() * 16) | 0, v = c == 'x' ? r : (r & 0x3) | 0x8;
|
|
353
|
-
return v.toString(16);
|
|
354
|
-
});
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
function addTrackIdIfDoesntExist(attrs) {
|
|
358
|
-
if (!attrs.id) {
|
|
359
|
-
return {
|
|
360
|
-
id: uuidv4(),
|
|
361
|
-
...attrs,
|
|
362
|
-
};
|
|
363
|
-
}
|
|
364
|
-
return attrs;
|
|
365
|
-
}
|
|
366
|
-
function getTextNodeTrackedMarkData(node, schema) {
|
|
367
|
-
if (!node || !node.isText) {
|
|
368
|
-
return undefined;
|
|
369
|
-
}
|
|
370
|
-
const marksTrackedData = [];
|
|
371
|
-
node.marks.forEach((mark) => {
|
|
372
|
-
if (mark.type === schema.marks.tracked_insert || mark.type === schema.marks.tracked_delete) {
|
|
373
|
-
const operation = mark.type === schema.marks.tracked_insert
|
|
374
|
-
? exports.CHANGE_OPERATION.insert
|
|
375
|
-
: exports.CHANGE_OPERATION.delete;
|
|
376
|
-
marksTrackedData.push({ ...mark.attrs.dataTracked, operation });
|
|
377
|
-
}
|
|
378
|
-
});
|
|
379
|
-
if (marksTrackedData.length > 1) {
|
|
380
|
-
log.warn('inline node with more than 1 of tracked marks', marksTrackedData);
|
|
381
|
-
}
|
|
382
|
-
return marksTrackedData[0] || undefined;
|
|
383
|
-
}
|
|
384
|
-
function getBlockInlineTrackedData(node) {
|
|
385
|
-
const { dataTracked } = node.attrs;
|
|
386
|
-
if (dataTracked && !Array.isArray(dataTracked)) {
|
|
387
|
-
return [dataTracked];
|
|
388
|
-
}
|
|
389
|
-
return dataTracked;
|
|
390
|
-
}
|
|
391
|
-
function getNodeTrackedData(node, schema) {
|
|
392
|
-
let tracked;
|
|
393
|
-
if (node && !node.isText) {
|
|
394
|
-
tracked = getBlockInlineTrackedData(node);
|
|
395
|
-
}
|
|
396
|
-
else if (node === null || node === void 0 ? void 0 : node.isText) {
|
|
397
|
-
tracked = getTextNodeTrackedMarkData(node, schema);
|
|
398
|
-
}
|
|
399
|
-
if (tracked && !Array.isArray(tracked)) {
|
|
400
|
-
tracked = [tracked];
|
|
401
|
-
}
|
|
402
|
-
return tracked;
|
|
403
|
-
}
|
|
404
|
-
function equalMarks(n1, n2) {
|
|
405
|
-
return (n1.marks.length === n2.marks.length &&
|
|
406
|
-
n1.marks.every((mark) => n1.marks.find((m) => m.type === mark.type)));
|
|
407
|
-
}
|
|
408
|
-
function shouldMergeTrackedAttributes(left, right) {
|
|
409
|
-
if (!left || !right) {
|
|
410
|
-
log.warn('passed undefined dataTracked attributes to shouldMergeTrackedAttributes', {
|
|
411
|
-
left,
|
|
412
|
-
right,
|
|
413
|
-
});
|
|
414
|
-
return false;
|
|
415
|
-
}
|
|
416
|
-
return (left.status === right.status &&
|
|
417
|
-
left.operation === right.operation &&
|
|
418
|
-
left.authorID === right.authorID);
|
|
419
|
-
}
|
|
420
|
-
function getMergeableMarkTrackedAttrs(node, attrs, schema) {
|
|
421
|
-
const nodeAttrs = getTextNodeTrackedMarkData(node, schema);
|
|
422
|
-
return nodeAttrs && shouldMergeTrackedAttributes(nodeAttrs, attrs) ? nodeAttrs : null;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
/*!
|
|
426
|
-
* © 2021 Atypon Systems LLC
|
|
427
|
-
*
|
|
428
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
429
|
-
* you may not use this file except in compliance with the License.
|
|
430
|
-
* You may obtain a copy of the License at
|
|
431
|
-
*
|
|
432
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
433
|
-
*
|
|
434
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
435
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
436
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
437
|
-
* See the License for the specific language governing permissions and
|
|
438
|
-
* limitations under the License.
|
|
439
|
-
*/
|
|
440
|
-
/**
|
|
441
|
-
* Deletes node but tries to leave its content intact by trying to unwrap it first
|
|
442
|
-
*
|
|
443
|
-
* Incase unwrapping doesn't work deletes the whole node.
|
|
444
|
-
* @param node
|
|
445
|
-
* @param pos
|
|
446
|
-
* @param tr
|
|
447
|
-
* @returns
|
|
448
|
-
*/
|
|
449
|
-
function deleteNode(node, pos, tr) {
|
|
450
|
-
var _a;
|
|
451
|
-
const startPos = tr.doc.resolve(pos + 1);
|
|
452
|
-
const range = startPos.blockRange(tr.doc.resolve(startPos.pos - 2 + node.nodeSize));
|
|
453
|
-
const targetDepth = range && prosemirrorTransform.liftTarget(range);
|
|
454
|
-
// Check with typeof since with prosemirror-transform pre 1.6.0 targetDepth is undefined
|
|
455
|
-
if (range && typeof targetDepth === 'number') {
|
|
456
|
-
return tr.lift(range, targetDepth);
|
|
457
|
-
}
|
|
458
|
-
const resPos = tr.doc.resolve(pos);
|
|
459
|
-
// Block nodes can be deleted by just removing their start token which should then merge the text
|
|
460
|
-
// content to above node's content (if there is one)
|
|
461
|
-
// this will work just for the node after the first child
|
|
462
|
-
const canMergeToNodeAbove = resPos.parent !== tr.doc && resPos.nodeBefore && node.isBlock && ((_a = node.firstChild) === null || _a === void 0 ? void 0 : _a.isText);
|
|
463
|
-
if (canMergeToNodeAbove) {
|
|
464
|
-
return tr.replaceWith(pos - 1, pos + 1, prosemirrorModel.Fragment.empty);
|
|
465
|
-
}
|
|
466
|
-
else {
|
|
467
|
-
// NOTE: there's an edge case where moving content is not possible but because the immediate
|
|
468
|
-
// child, say some wrapper blockNode, is also deleted the content could be retained. TODO I guess.
|
|
469
|
-
return tr.delete(pos, pos + node.nodeSize);
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
/**
|
|
473
|
-
* Deletes inserted block or inline node, otherwise adds `dataTracked` object with CHANGE_STATUS 'deleted'
|
|
474
|
-
* @param node
|
|
475
|
-
* @param pos
|
|
476
|
-
* @param newTr
|
|
477
|
-
* @param deleteAttrs
|
|
478
|
-
*/
|
|
479
|
-
function deleteOrSetNodeDeleted(node, pos, newTr, deleteAttrs) {
|
|
480
|
-
const dataTracked = getBlockInlineTrackedData(node);
|
|
481
|
-
const inserted = dataTracked === null || dataTracked === void 0 ? void 0 : dataTracked.find((d) => d.operation === exports.CHANGE_OPERATION.insert);
|
|
482
|
-
const deleted = dataTracked === null || dataTracked === void 0 ? void 0 : dataTracked.find((d) => d.operation === exports.CHANGE_OPERATION.delete);
|
|
483
|
-
const updated = dataTracked === null || dataTracked === void 0 ? void 0 : dataTracked.find((d) => d.operation === exports.CHANGE_OPERATION.set_node_attributes);
|
|
484
|
-
if (inserted && inserted.authorID === deleteAttrs.authorID) {
|
|
485
|
-
return deleteNode(node, pos, newTr);
|
|
486
|
-
}
|
|
487
|
-
const newDeleted = deleted
|
|
488
|
-
? { ...deleted, updatedAt: deleteAttrs.updatedAt }
|
|
489
|
-
: addTrackIdIfDoesntExist(deleteAttrs);
|
|
490
|
-
newTr.setNodeMarkup(pos, undefined, {
|
|
491
|
-
...node.attrs,
|
|
492
|
-
dataTracked: updated ? [newDeleted, updated] : [newDeleted],
|
|
493
|
-
}, node.marks);
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
/*!
|
|
497
|
-
* © 2021 Atypon Systems LLC
|
|
498
|
-
*
|
|
499
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
500
|
-
* you may not use this file except in compliance with the License.
|
|
501
|
-
* You may obtain a copy of the License at
|
|
502
|
-
*
|
|
503
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
504
|
-
*
|
|
505
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
506
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
507
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
508
|
-
* See the License for the specific language governing permissions and
|
|
509
|
-
* limitations under the License.
|
|
510
|
-
*/
|
|
511
|
-
/**
|
|
512
|
-
* Deletes node but tries to leave its content intact by moving/wrapping it to a node before or after
|
|
513
|
-
* @param node
|
|
514
|
-
* @param pos
|
|
515
|
-
* @param tr
|
|
516
|
-
* @returns
|
|
517
|
-
*/
|
|
518
|
-
function mergeNode(node, pos, tr) {
|
|
519
|
-
var _a;
|
|
520
|
-
if (prosemirrorTransform.canJoin(tr.doc, pos)) {
|
|
521
|
-
return tr.join(pos);
|
|
522
|
-
}
|
|
523
|
-
else if (!tr.doc.resolve(pos).nodeBefore) {
|
|
524
|
-
// for this case will just delete that node in `deleteNode.ts` as the join will not work
|
|
525
|
-
return undefined;
|
|
526
|
-
}
|
|
527
|
-
// TODO is this the same thing as join to above?
|
|
528
|
-
const resPos = tr.doc.resolve(pos);
|
|
529
|
-
const canMergeToNodeAbove = (resPos.parent !== tr.doc || resPos.nodeBefore) && ((_a = node.firstChild) === null || _a === void 0 ? void 0 : _a.isText);
|
|
530
|
-
if (canMergeToNodeAbove) {
|
|
531
|
-
return tr.replaceWith(pos - 1, pos + 1, prosemirrorModel.Fragment.empty);
|
|
532
|
-
}
|
|
533
|
-
return undefined;
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
function updateChangeAttrs(tr, change, trackedAttrs, schema) {
|
|
537
|
-
const node = tr.doc.nodeAt(change.from);
|
|
538
|
-
if (!node) {
|
|
539
|
-
log.error('updateChangeAttrs: no node at the from of change ', change);
|
|
540
|
-
return tr;
|
|
541
|
-
}
|
|
542
|
-
const { operation } = trackedAttrs;
|
|
543
|
-
const oldTrackData = change.type === 'text-change' ? getTextNodeTrackedMarkData(node, schema) : getBlockInlineTrackedData(node);
|
|
544
|
-
if (!operation) {
|
|
545
|
-
log.warn('updateChangeAttrs: unable to determine operation of change ', change);
|
|
546
|
-
}
|
|
547
|
-
else if (!oldTrackData) {
|
|
548
|
-
log.warn('updateChangeAttrs: no old dataTracked for change ', change);
|
|
549
|
-
}
|
|
550
|
-
if (change.type === 'text-change') {
|
|
551
|
-
const oldMark = node.marks.find((m) => m.type === schema.marks.tracked_insert || m.type === schema.marks.tracked_delete);
|
|
552
|
-
if (!oldMark) {
|
|
553
|
-
log.warn('updateChangeAttrs: no track marks for a text-change ', change);
|
|
554
|
-
return tr;
|
|
555
|
-
}
|
|
556
|
-
// TODO add operation based on mark type if it's undefined?
|
|
557
|
-
tr.addMark(change.from, change.to, oldMark.type.create({ ...oldMark.attrs, dataTracked: { ...oldTrackData, ...trackedAttrs } }));
|
|
558
|
-
}
|
|
559
|
-
else if ((change.type === 'node-change' || change.type === 'node-attr-change') && !operation) {
|
|
560
|
-
// Very weird edge-case if this happens
|
|
561
|
-
tr.setNodeMarkup(change.from, undefined, { ...node.attrs, dataTracked: null }, node.marks);
|
|
562
|
-
}
|
|
563
|
-
else if (change.type === 'node-change' || change.type === 'node-attr-change') {
|
|
564
|
-
const trackedDataSource = getBlockInlineTrackedData(node) || [];
|
|
565
|
-
const targetDataTracked = trackedDataSource.find((t) => change.id === t.id);
|
|
566
|
-
const newDataTracked = trackedDataSource.map((oldTrack) => {
|
|
567
|
-
if (targetDataTracked) {
|
|
568
|
-
if (oldTrack.id === targetDataTracked.id) {
|
|
569
|
-
return { ...oldTrack, ...trackedAttrs };
|
|
570
|
-
}
|
|
571
|
-
return oldTrack;
|
|
572
|
-
}
|
|
573
|
-
if (oldTrack.operation === operation) {
|
|
574
|
-
return { ...oldTrack, ...trackedAttrs };
|
|
575
|
-
}
|
|
576
|
-
return oldTrack;
|
|
577
|
-
});
|
|
578
|
-
tr.setNodeMarkup(change.from, undefined, { ...node.attrs, dataTracked: newDataTracked.length === 0 ? null : newDataTracked }, node.marks);
|
|
579
|
-
}
|
|
580
|
-
return tr;
|
|
581
|
-
}
|
|
582
|
-
function updateChangeChildrenAttributes(changes, tr, mapping) {
|
|
583
|
-
changes.forEach((c) => {
|
|
584
|
-
if (c.type === 'node-change' && !ChangeSet.shouldDeleteChange(c)) {
|
|
585
|
-
const from = mapping.map(c.from);
|
|
586
|
-
const node = tr.doc.nodeAt(from);
|
|
587
|
-
if (!node) {
|
|
588
|
-
return;
|
|
589
|
-
}
|
|
590
|
-
const attrs = { ...node.attrs, dataTracked: null };
|
|
591
|
-
tr.setNodeMarkup(from, undefined, attrs, node.marks);
|
|
592
|
-
}
|
|
593
|
-
});
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
function getUpdatedDataTracked(dataTracked, changeId) {
|
|
597
|
-
if (!dataTracked) {
|
|
598
|
-
return null;
|
|
599
|
-
}
|
|
600
|
-
const newDataTracked = dataTracked.filter((c) => c.id !== changeId);
|
|
601
|
-
return newDataTracked.length ? newDataTracked : null;
|
|
602
|
-
}
|
|
603
|
-
/**
|
|
604
|
-
* Applies the accepted/rejected changes in the current document and sets them untracked
|
|
605
|
-
*
|
|
606
|
-
* @param tr
|
|
607
|
-
* @param schema
|
|
608
|
-
* @param changes
|
|
609
|
-
* @param deleteMap
|
|
610
|
-
*/
|
|
611
|
-
function applyAcceptedRejectedChanges(tr, schema, changes, deleteMap = new prosemirrorTransform.Mapping()) {
|
|
612
|
-
const attrsChangesLog = new Map(); // map of node ids and applied change updatedAt timestamp
|
|
613
|
-
function addAttrLog(nodeId, changeId) {
|
|
614
|
-
const arr = attrsChangesLog.get(nodeId) || attrsChangesLog.set(nodeId, []).get(nodeId);
|
|
615
|
-
arr.push(changeId);
|
|
616
|
-
}
|
|
617
|
-
changes.forEach((change) => {
|
|
618
|
-
// Map change.from and skip those which dont need to be applied
|
|
619
|
-
// or were already deleted by an applied block delete
|
|
620
|
-
const { pos: from, deleted } = deleteMap.mapResult(change.from), node = tr.doc.nodeAt(from), noChangeNeeded = deleted || !ChangeSet.shouldDeleteChange(change);
|
|
621
|
-
if (!node) {
|
|
622
|
-
!deleted && log.warn('no node found to update for change', change);
|
|
623
|
-
return;
|
|
624
|
-
}
|
|
625
|
-
if (change.dataTracked.status === exports.CHANGE_STATUS.pending) {
|
|
626
|
-
if (ChangeSet.isNodeAttrChange(change)) {
|
|
627
|
-
/*
|
|
628
|
-
Apply pending changes for attributes as well because applying accepted/rejected changes may override
|
|
629
|
-
pending because we store the most recent change directly on the node attributes. But in case of pending attributes
|
|
630
|
-
we don't need to remove dataTracked record. We need, however, to make sure we don't restore dataTracked records for
|
|
631
|
-
the previously applied changes. To check for already applied changes we log them into "attrsChangesLog" Map.
|
|
632
|
-
*/
|
|
633
|
-
const { dataTracked, ...attrs } = change.newAttrs;
|
|
634
|
-
const changeLog = attrsChangesLog.get(node.attrs.id);
|
|
635
|
-
const newDataTracked = changeLog && changeLog.length ? dataTracked.filter((c) => !changeLog.includes(c.id)) : dataTracked;
|
|
636
|
-
tr.setNodeMarkup(from, undefined, { ...attrs, dataTracked: newDataTracked.length ? newDataTracked : null }, node.marks);
|
|
637
|
-
// default is "null" for dataTracked in attrs in pm schema, so codebase generally relies on it being null when empty
|
|
638
|
-
}
|
|
639
|
-
return;
|
|
640
|
-
}
|
|
641
|
-
if (ChangeSet.isTextChange(change) && noChangeNeeded) {
|
|
642
|
-
tr.removeMark(from, deleteMap.map(change.to), schema.marks.tracked_insert);
|
|
643
|
-
tr.removeMark(from, deleteMap.map(change.to), schema.marks.tracked_delete);
|
|
644
|
-
}
|
|
645
|
-
else if (ChangeSet.isTextChange(change)) {
|
|
646
|
-
tr.delete(from, deleteMap.map(change.to));
|
|
647
|
-
deleteMap.appendMap(tr.steps[tr.steps.length - 1].getMap());
|
|
648
|
-
}
|
|
649
|
-
else if (ChangeSet.isNodeChange(change) && noChangeNeeded) {
|
|
650
|
-
const attrs = { ...node.attrs, dataTracked: null };
|
|
651
|
-
tr.setNodeMarkup(from, undefined, attrs, node.marks);
|
|
652
|
-
updateChangeChildrenAttributes(change.children, tr, deleteMap);
|
|
653
|
-
}
|
|
654
|
-
else if (ChangeSet.isNodeChange(change)) {
|
|
655
|
-
// Try first moving the node children to either nodeAbove, nodeBelow or its parent.
|
|
656
|
-
// Then try unwrapping it with lift or just hacky-joining by replacing the border between
|
|
657
|
-
// it and its parent with Fragment.empty. If none of these apply, delete the content between the change.
|
|
658
|
-
const merged = mergeNode(node, from, tr);
|
|
659
|
-
if (merged === undefined) {
|
|
660
|
-
deleteNode(node, from, tr);
|
|
661
|
-
}
|
|
662
|
-
deleteMap.appendMap(tr.steps[tr.steps.length - 1].getMap());
|
|
663
|
-
}
|
|
664
|
-
else if (ChangeSet.isNodeAttrChange(change) && change.dataTracked.status === exports.CHANGE_STATUS.accepted) {
|
|
665
|
-
tr.setNodeMarkup(from, undefined, { ...change.newAttrs, dataTracked: getUpdatedDataTracked(node.attrs.dataTracked, change.id) }, node.marks);
|
|
666
|
-
addAttrLog(node.attrs.id, change.dataTracked.id);
|
|
667
|
-
}
|
|
668
|
-
else if (ChangeSet.isNodeAttrChange(change) && change.dataTracked.status === exports.CHANGE_STATUS.rejected) {
|
|
669
|
-
tr.setNodeMarkup(from, undefined, { ...change.oldAttrs, dataTracked: getUpdatedDataTracked(node.attrs.dataTracked, change.id) }, node.marks);
|
|
670
|
-
addAttrLog(node.attrs.id, change.dataTracked.id);
|
|
671
|
-
}
|
|
672
|
-
});
|
|
673
|
-
return deleteMap;
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
/**
|
|
677
|
-
* Finds all changes (basically text marks or node attributes) from document
|
|
678
|
-
*
|
|
679
|
-
* This could be possibly made more efficient by only iterating the sections of doc where changes have
|
|
680
|
-
* been applied. This could attempted with eg findDiffStart but it might be less robust than just using
|
|
681
|
-
* doc.descendants
|
|
682
|
-
* @param state
|
|
683
|
-
* @returns
|
|
684
|
-
*/
|
|
685
|
-
function findChanges(state) {
|
|
686
|
-
const changes = [];
|
|
687
|
-
// Store the last iterated change to join adjacent text changes
|
|
688
|
-
let current;
|
|
689
|
-
state.doc.descendants((node, pos) => {
|
|
690
|
-
const tracked = getNodeTrackedData(node, state.schema) || [];
|
|
691
|
-
for (let i = 0; i < tracked.length; i += 1) {
|
|
692
|
-
const dataTracked = tracked[i];
|
|
693
|
-
const id = dataTracked.id || '';
|
|
694
|
-
// Join adjacent text changes that have been broken up due to different marks
|
|
695
|
-
// eg <ins><b>bold</b>norm<i>italic</i></ins> -> treated as one continuous change
|
|
696
|
-
// Note the !equalMarks to leave changes separate incase the marks are equal to let fixInconsistentChanges to fix them
|
|
697
|
-
if (current &&
|
|
698
|
-
current.change.id === id &&
|
|
699
|
-
current.node.isText &&
|
|
700
|
-
node.isText &&
|
|
701
|
-
!equalMarks(node, current.node)) {
|
|
702
|
-
current.change.to = pos + node.nodeSize;
|
|
703
|
-
// Important to update the node as the text changes might contain multiple parts where some marks equal each other
|
|
704
|
-
current.node = node;
|
|
705
|
-
continue;
|
|
706
|
-
}
|
|
707
|
-
current && changes.push(current.change);
|
|
708
|
-
let change;
|
|
709
|
-
if (node.isText) {
|
|
710
|
-
change = {
|
|
711
|
-
id,
|
|
712
|
-
type: 'text-change',
|
|
713
|
-
from: pos,
|
|
714
|
-
to: pos + node.nodeSize,
|
|
715
|
-
dataTracked,
|
|
716
|
-
text: node.text,
|
|
717
|
-
};
|
|
718
|
-
}
|
|
719
|
-
else if (dataTracked.operation === exports.CHANGE_OPERATION.set_node_attributes) {
|
|
720
|
-
change = {
|
|
721
|
-
id,
|
|
722
|
-
type: 'node-attr-change',
|
|
723
|
-
from: pos,
|
|
724
|
-
to: pos + node.nodeSize,
|
|
725
|
-
dataTracked,
|
|
726
|
-
nodeType: node.type.name,
|
|
727
|
-
newAttrs: node.attrs,
|
|
728
|
-
oldAttrs: dataTracked.oldAttrs,
|
|
729
|
-
};
|
|
730
|
-
}
|
|
731
|
-
else {
|
|
732
|
-
change = {
|
|
733
|
-
id,
|
|
734
|
-
type: 'node-change',
|
|
735
|
-
from: pos,
|
|
736
|
-
to: pos + node.nodeSize,
|
|
737
|
-
dataTracked,
|
|
738
|
-
nodeType: node.type.name,
|
|
739
|
-
children: [],
|
|
740
|
-
};
|
|
741
|
-
}
|
|
742
|
-
current = {
|
|
743
|
-
change,
|
|
744
|
-
node,
|
|
745
|
-
};
|
|
746
|
-
}
|
|
747
|
-
if (tracked.length === 0 && current) {
|
|
748
|
-
changes.push(current.change);
|
|
749
|
-
current = undefined;
|
|
750
|
-
}
|
|
751
|
-
});
|
|
752
|
-
current && changes.push(current.change);
|
|
753
|
-
return new ChangeSet(changes);
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
/**
|
|
757
|
-
* Iterates over a ChangeSet to check all changes have their required attributes
|
|
758
|
-
*
|
|
759
|
-
* 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.
|
|
760
|
-
* @param changeSet
|
|
761
|
-
* @param currentUser
|
|
762
|
-
* @param newTr
|
|
763
|
-
* @param schema
|
|
764
|
-
* @return docWasChanged, a boolean
|
|
765
|
-
*/
|
|
766
|
-
function fixInconsistentChanges(changeSet, currentUserID, newTr, schema) {
|
|
767
|
-
const iteratedIds = new Set();
|
|
768
|
-
let changed = false;
|
|
769
|
-
changeSet.invalidChanges.forEach((c) => {
|
|
770
|
-
const { id, authorID, operation, reviewedByID, status, createdAt, updatedAt } = c.dataTracked;
|
|
771
|
-
const newAttrs = {
|
|
772
|
-
...((!id || iteratedIds.has(id) || id.length === 0) && { id: uuidv4() }),
|
|
773
|
-
...(!authorID && { authorID: currentUserID }),
|
|
774
|
-
// Dont add a default operation -> rather have updateChangeAttrs delete the track data
|
|
775
|
-
// ...(!operation && { operation: CHANGE_OPERATION.insert }),
|
|
776
|
-
...(!reviewedByID && { reviewedByID: null }),
|
|
777
|
-
...(!status && { status: exports.CHANGE_STATUS.pending }),
|
|
778
|
-
...(!createdAt && { createdAt: Date.now() }),
|
|
779
|
-
...(!updatedAt && { updatedAt: Date.now() }),
|
|
780
|
-
};
|
|
781
|
-
if (Object.keys(newAttrs).length > 0) {
|
|
782
|
-
updateChangeAttrs(newTr, c, { ...c.dataTracked, ...newAttrs }, schema);
|
|
783
|
-
changed = true;
|
|
784
|
-
}
|
|
785
|
-
iteratedIds.add(newAttrs.id || id);
|
|
786
|
-
});
|
|
787
|
-
return changed;
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
/*!
|
|
791
|
-
* © 2021 Atypon Systems LLC
|
|
792
|
-
*
|
|
793
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
794
|
-
* you may not use this file except in compliance with the License.
|
|
795
|
-
* You may obtain a copy of the License at
|
|
796
|
-
*
|
|
797
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
798
|
-
*
|
|
799
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
800
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
801
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
802
|
-
* See the License for the specific language governing permissions and
|
|
803
|
-
* limitations under the License.
|
|
804
|
-
*/
|
|
805
|
-
/**
|
|
806
|
-
* Recurses node children and returns the merged first/last node's content and the unmerged children
|
|
807
|
-
*
|
|
808
|
-
* For example when merging two blockquotes:
|
|
809
|
-
* <bq><p>old|</p></bq>...| + [<bq><p>] inserted</p><p>2nd p</p></bq> -> <bq><p>old inserted</p><p>2nd p</p></bq>
|
|
810
|
-
* The extracted merged and unmerged content from the insertSlice are:
|
|
811
|
-
* {
|
|
812
|
-
* mergedNodeContent: <text> inserted</text>
|
|
813
|
-
* unmergedContent: [<p>2nd p</p>]
|
|
814
|
-
* }
|
|
815
|
-
* @param node
|
|
816
|
-
* @param currentDepth
|
|
817
|
-
* @param depth
|
|
818
|
-
* @param first
|
|
819
|
-
* @returns
|
|
820
|
-
*/
|
|
821
|
-
function getMergedNode(node, currentDepth, depth, first) {
|
|
822
|
-
if (currentDepth === depth) {
|
|
823
|
-
return {
|
|
824
|
-
mergedNodeContent: node.content,
|
|
825
|
-
unmergedContent: undefined,
|
|
826
|
-
};
|
|
827
|
-
}
|
|
828
|
-
const result = [];
|
|
829
|
-
let merged = prosemirrorModel.Fragment.empty;
|
|
830
|
-
node.content.forEach((n, _, i) => {
|
|
831
|
-
if ((first && i === 0) || (!first && i === node.childCount - 1)) {
|
|
832
|
-
const { mergedNodeContent, unmergedContent } = getMergedNode(n, currentDepth + 1, depth, first);
|
|
833
|
-
merged = mergedNodeContent;
|
|
834
|
-
if (unmergedContent) {
|
|
835
|
-
result.push(...unmergedContent.content);
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
else {
|
|
839
|
-
result.push(n);
|
|
840
|
-
}
|
|
841
|
-
});
|
|
842
|
-
return {
|
|
843
|
-
mergedNodeContent: merged,
|
|
844
|
-
unmergedContent: result.length > 0 ? prosemirrorModel.Fragment.fromArray(result) : undefined,
|
|
845
|
-
};
|
|
846
|
-
}
|
|
847
|
-
/**
|
|
848
|
-
* Filters merged nodes from an open insertSlice to manually merge them to prevent unwanted deletions
|
|
849
|
-
*
|
|
850
|
-
* So instead of joining the slice by its open sides, possibly deleting previous nodes, we can push the
|
|
851
|
-
* changed content manually inside the merged nodes.
|
|
852
|
-
* Eg. instead of doing `|<p>asdf</p><p>|bye</p>` automatically, we extract the merged nodes first:
|
|
853
|
-
* {
|
|
854
|
-
* updatedSliceNodes: [<p>asdf</p>],
|
|
855
|
-
* firstMergedNode: <p>bye</p>,
|
|
856
|
-
* lastMergedNode: undefined,
|
|
857
|
-
* }
|
|
858
|
-
* @param insertSlice inserted slice
|
|
859
|
-
*/
|
|
860
|
-
function splitSliceIntoMergedParts(insertSlice, mergeEqualSides = false) {
|
|
861
|
-
const { openStart, openEnd, content: { firstChild, lastChild, content: nodes }, } = insertSlice;
|
|
862
|
-
let updatedSliceNodes = nodes;
|
|
863
|
-
const mergeSides = openStart !== openEnd || mergeEqualSides;
|
|
864
|
-
const firstMergedNode = openStart > 0 && mergeSides && firstChild
|
|
865
|
-
? getMergedNode(firstChild, 1, openStart, true)
|
|
866
|
-
: undefined;
|
|
867
|
-
const lastMergedNode = openEnd > 0 && mergeSides && lastChild ? getMergedNode(lastChild, 1, openEnd, false) : undefined;
|
|
868
|
-
if (firstMergedNode) {
|
|
869
|
-
updatedSliceNodes = updatedSliceNodes.slice(1);
|
|
870
|
-
if (firstMergedNode.unmergedContent) {
|
|
871
|
-
updatedSliceNodes = [...firstMergedNode.unmergedContent.content, ...updatedSliceNodes];
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
if (lastMergedNode) {
|
|
875
|
-
updatedSliceNodes = updatedSliceNodes.slice(0, -1);
|
|
876
|
-
if (lastMergedNode.unmergedContent) {
|
|
877
|
-
updatedSliceNodes = [...updatedSliceNodes, ...lastMergedNode.unmergedContent.content];
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
return {
|
|
881
|
-
updatedSliceNodes,
|
|
882
|
-
firstMergedNode,
|
|
883
|
-
lastMergedNode,
|
|
884
|
-
};
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
/*!
|
|
888
|
-
* © 2021 Atypon Systems LLC
|
|
889
|
-
*
|
|
890
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
891
|
-
* you may not use this file except in compliance with the License.
|
|
892
|
-
* You may obtain a copy of the License at
|
|
893
|
-
*
|
|
894
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
895
|
-
*
|
|
896
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
897
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
898
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
899
|
-
* See the License for the specific language governing permissions and
|
|
900
|
-
* limitations under the License.
|
|
901
|
-
*/
|
|
902
|
-
function markInlineNodeChange(node, newTrackAttrs, schema) {
|
|
903
|
-
const filtered = node.marks.filter((m) => m.type !== schema.marks.tracked_insert && m.type !== schema.marks.tracked_delete);
|
|
904
|
-
const mark = newTrackAttrs.operation === exports.CHANGE_OPERATION.insert
|
|
905
|
-
? schema.marks.tracked_insert
|
|
906
|
-
: schema.marks.tracked_delete;
|
|
907
|
-
const createdMark = mark.create({
|
|
908
|
-
dataTracked: addTrackIdIfDoesntExist(newTrackAttrs),
|
|
909
|
-
});
|
|
910
|
-
return node.mark(filtered.concat(createdMark));
|
|
911
|
-
}
|
|
912
|
-
/**
|
|
913
|
-
* Iterates over fragment's content and joins pasted text with old track marks
|
|
914
|
-
*
|
|
915
|
-
* This is not strictly necessary but it's kinda bad UX if the inserted text is split into parts
|
|
916
|
-
* even when it's authored by the same user.
|
|
917
|
-
* @param content
|
|
918
|
-
* @param newTrackAttrs
|
|
919
|
-
* @param schema
|
|
920
|
-
* @returns
|
|
921
|
-
*/
|
|
922
|
-
function loopContentAndMergeText(content, newTrackAttrs, schema) {
|
|
923
|
-
var _a;
|
|
924
|
-
const updatedChildren = [];
|
|
925
|
-
for (let i = 0; i < content.childCount; i += 1) {
|
|
926
|
-
const recursed = recurseNodeContent(content.child(i), newTrackAttrs, schema);
|
|
927
|
-
const prev = i > 0 ? updatedChildren[i - 1] : null;
|
|
928
|
-
if ((prev === null || prev === void 0 ? void 0 : prev.isText) &&
|
|
929
|
-
recursed.isText &&
|
|
930
|
-
equalMarks(prev, recursed) &&
|
|
931
|
-
((_a = getTextNodeTrackedMarkData(prev, schema)) === null || _a === void 0 ? void 0 : _a.operation) === exports.CHANGE_OPERATION.insert) {
|
|
932
|
-
updatedChildren.splice(i - 1, 1, schema.text('' + prev.text + recursed.text, prev.marks));
|
|
933
|
-
}
|
|
934
|
-
else {
|
|
935
|
-
updatedChildren.push(recursed);
|
|
936
|
-
}
|
|
937
|
-
}
|
|
938
|
-
return updatedChildren;
|
|
939
|
-
}
|
|
940
|
-
function recurseNodeContent(node, newTrackAttrs, schema) {
|
|
941
|
-
if (node.isText) {
|
|
942
|
-
return markInlineNodeChange(node, newTrackAttrs, schema);
|
|
943
|
-
}
|
|
944
|
-
else if (node.isBlock || node.isInline) {
|
|
945
|
-
const updatedChildren = loopContentAndMergeText(node.content, newTrackAttrs, schema);
|
|
946
|
-
return node.type.create({
|
|
947
|
-
...node.attrs,
|
|
948
|
-
dataTracked: [addTrackIdIfDoesntExist(newTrackAttrs)],
|
|
949
|
-
}, prosemirrorModel.Fragment.fromArray(updatedChildren), node.marks);
|
|
950
|
-
}
|
|
951
|
-
else {
|
|
952
|
-
log.error(`unhandled node type: "${node.type.name}"`, node);
|
|
953
|
-
return node;
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
function setFragmentAsInserted(inserted, insertAttrs, schema) {
|
|
957
|
-
// Recurse the content in the inserted slice and either mark it tracked_insert or set node attrs
|
|
958
|
-
const updatedInserted = loopContentAndMergeText(inserted, insertAttrs, schema);
|
|
959
|
-
return updatedInserted.length === 0 ? prosemirrorModel.Fragment.empty : prosemirrorModel.Fragment.fromArray(updatedInserted);
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
/*!
|
|
963
|
-
* © 2021 Atypon Systems LLC
|
|
964
|
-
*
|
|
965
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
966
|
-
* you may not use this file except in compliance with the License.
|
|
967
|
-
* You may obtain a copy of the License at
|
|
968
|
-
*
|
|
969
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
970
|
-
*
|
|
971
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
972
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
973
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
974
|
-
* See the License for the specific language governing permissions and
|
|
975
|
-
* limitations under the License.
|
|
976
|
-
*/
|
|
977
|
-
function createNewInsertAttrs(attrs) {
|
|
978
|
-
return {
|
|
979
|
-
...attrs,
|
|
980
|
-
operation: exports.CHANGE_OPERATION.insert,
|
|
981
|
-
};
|
|
982
|
-
}
|
|
983
|
-
function createNewDeleteAttrs(attrs) {
|
|
984
|
-
return {
|
|
985
|
-
...attrs,
|
|
986
|
-
operation: exports.CHANGE_OPERATION.delete,
|
|
987
|
-
};
|
|
988
|
-
}
|
|
989
|
-
function createNewUpdateAttrs(attrs, oldAttrs) {
|
|
990
|
-
// Omit dataTracked
|
|
991
|
-
const { dataTracked, ...restAttrs } = oldAttrs;
|
|
992
|
-
return {
|
|
993
|
-
...attrs,
|
|
994
|
-
operation: exports.CHANGE_OPERATION.set_node_attributes,
|
|
995
|
-
oldAttrs: JSON.parse(JSON.stringify(restAttrs)),
|
|
996
|
-
};
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
/*!
|
|
1000
|
-
* © 2021 Atypon Systems LLC
|
|
1001
|
-
*
|
|
1002
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
1003
|
-
* you may not use this file except in compliance with the License.
|
|
1004
|
-
* You may obtain a copy of the License at
|
|
1005
|
-
*
|
|
1006
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
1007
|
-
*
|
|
1008
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
1009
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
1010
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
1011
|
-
* See the License for the specific language governing permissions and
|
|
1012
|
-
* limitations under the License.
|
|
1013
|
-
*/
|
|
1014
|
-
/**
|
|
1015
|
-
* Applies deletion to the doc without actually deleting nodes that have not been inserted
|
|
1016
|
-
*
|
|
1017
|
-
* The hairiest part of this whole library which does a fair bit of magic to split the inserted slice
|
|
1018
|
-
* into pieces that can be inserted without deleting nodes in the doc. Basically we first split the
|
|
1019
|
-
* inserted slice into merged pieces _if_ the slice was open on either end. Then, we iterate over the deleted
|
|
1020
|
-
* range and see if the node in question was completely wrapped in the range (therefore fully deleted)
|
|
1021
|
-
* or only partially deleted by the slice. In that case, we merge the content from the inserted slice
|
|
1022
|
-
* and keep the original nodes if they do not contain insert attributes.
|
|
1023
|
-
*
|
|
1024
|
-
* It is definitely a messy function but so far this seems to have been the best approach to prevent
|
|
1025
|
-
* deletion of nodes with open slices. Other option would be to allow the deletions to take place but that
|
|
1026
|
-
* requires then inserting the deleted nodes back to the doc if their deletion should be prevented, which does
|
|
1027
|
-
* not seem trivial either.
|
|
1028
|
-
*
|
|
1029
|
-
* @param from start of the deleted range
|
|
1030
|
-
* @param to end of the deleted range
|
|
1031
|
-
* @param gap retained content in a ReplaceAroundStep, not deleted
|
|
1032
|
-
* @param startDoc doc before the deletion
|
|
1033
|
-
* @param newTr the new track transaction
|
|
1034
|
-
* @param schema ProseMirror schema
|
|
1035
|
-
* @param deleteAttrs attributes for the dataTracked object
|
|
1036
|
-
* @param insertSlice the inserted slice from ReplaceStep
|
|
1037
|
-
* @returns mapping adjusted by the applied operations & modified insert slice
|
|
1038
|
-
*/
|
|
1039
|
-
function deleteAndMergeSplitNodes(from, to, gap, startDoc, newTr, schema, trackAttrs, insertSlice) {
|
|
1040
|
-
const steps = [];
|
|
1041
|
-
// No deletion applied, return default values
|
|
1042
|
-
if (from === to) {
|
|
1043
|
-
return {
|
|
1044
|
-
newSliceContent: insertSlice.content,
|
|
1045
|
-
sliceWasSplit: false,
|
|
1046
|
-
steps,
|
|
1047
|
-
};
|
|
1048
|
-
}
|
|
1049
|
-
const { openStart, openEnd } = insertSlice;
|
|
1050
|
-
const { updatedSliceNodes, firstMergedNode, lastMergedNode } = splitSliceIntoMergedParts(insertSlice, gap !== undefined);
|
|
1051
|
-
let mergingStartSide = true;
|
|
1052
|
-
startDoc.nodesBetween(from, to, (node, pos) => {
|
|
1053
|
-
const nodeEnd = pos + node.nodeSize;
|
|
1054
|
-
// So this insane boolean checks for ReplaceAroundStep gaps and whether the node should be skipped
|
|
1055
|
-
// since the content inside gap should stay unchanged.
|
|
1056
|
-
// All other nodes except text nodes consist of one start and end token (or just a single token for atoms).
|
|
1057
|
-
// For them we can just check whether the start token is within the gap eg pos is 10 when gap (8, 18) to
|
|
1058
|
-
// determine whether it should be skipped.
|
|
1059
|
-
// For text nodes though, since they are continous, they might only partially be enclosed in the gap
|
|
1060
|
-
// eg. pos 10 when gap is (8, 18) BUT if their nodeEnd goes past the gap's end eg nodeEnd 20 they actually
|
|
1061
|
-
// are altered and should not be skipped.
|
|
1062
|
-
// @TODO ATM 20.7.2022 there doesn't seem to be tests that capture this.
|
|
1063
|
-
const wasWithinGap = gap &&
|
|
1064
|
-
((!node.isText && pos >= gap.start) ||
|
|
1065
|
-
(node.isText && pos >= gap.start && nodeEnd <= gap.end));
|
|
1066
|
-
// nodeEnd > offsetFrom -> delete touches this node
|
|
1067
|
-
// eg (del 6 10) <p 5>|<t 6>cdf</t 9></p 10>| -> <p> nodeEnd 10 > from 6
|
|
1068
|
-
if (nodeEnd > from && !wasWithinGap) {
|
|
1069
|
-
// |<p>asdf</p>| -> node deleted completely
|
|
1070
|
-
const nodeCompletelyDeleted = pos >= from && nodeEnd <= to;
|
|
1071
|
-
// The end token deleted eg:
|
|
1072
|
-
// <p 1>asdf|</p 7><p 7>bye</p 12>| + [<p>]hello</p> -> <p>asdfhello</p>
|
|
1073
|
-
// (del 6 12) + (ins [<p>]hello</p> openStart 1 openEnd 0)
|
|
1074
|
-
// (<p> nodeEnd 7) > (from 6) && (nodeEnd 7) <= (to 12)
|
|
1075
|
-
//
|
|
1076
|
-
// How about
|
|
1077
|
-
// <p 1>asdf|</p 7><p 7>|bye</p 12> + [<p>]hello</p><p>good[</p>] -> <p>asdfhello</p><p>goodbye</p>
|
|
1078
|
-
//
|
|
1079
|
-
// What about:
|
|
1080
|
-
// <p 1>asdf|</p 7><p 7 op="inserted">|bye</p 12> + empty -> <p>asdfbye</p>
|
|
1081
|
-
const endTokenDeleted = nodeEnd <= to;
|
|
1082
|
-
// The start token deleted eg:
|
|
1083
|
-
// |<p1 0>hey</p 6><p2 6>|asdf</p 12> + <p3>hello [</p>] -> <p3>hello asdf</p2>
|
|
1084
|
-
// (del 0 7) + (ins <p>hello [</p>] openStart 0 openEnd 1)
|
|
1085
|
-
// (<p1> pos 0) >= (from 0) && (nodeEnd 6) - 1 > (to 7) == false???
|
|
1086
|
-
// (<p2> pos 6) >= (from 0) && (nodeEnd 12) - 1 > (to 7) == true
|
|
1087
|
-
//
|
|
1088
|
-
const startTokenDeleted = pos >= from; // && nodeEnd - 1 > offsetTo
|
|
1089
|
-
if (node.isText ||
|
|
1090
|
-
(!endTokenDeleted && startTokenDeleted) ||
|
|
1091
|
-
(endTokenDeleted && !startTokenDeleted)) {
|
|
1092
|
-
// Since we don't know which side to merge with wholly deleted TextNodes, we use this boolean to remember
|
|
1093
|
-
// whether we have entered the endSide of the mergeable blockNodes. Also applies for partial TextNodes
|
|
1094
|
-
// (which we could determine without this).
|
|
1095
|
-
if (!endTokenDeleted && startTokenDeleted) {
|
|
1096
|
-
mergingStartSide = false;
|
|
1097
|
-
}
|
|
1098
|
-
// Depth is often 1 when merging paragraphs or 2 for fully open blockquotes.
|
|
1099
|
-
// Incase of merging text within a ReplaceAroundStep the depth might be 1
|
|
1100
|
-
const depth = newTr.doc.resolve(pos).depth;
|
|
1101
|
-
const mergeContent = mergingStartSide
|
|
1102
|
-
? firstMergedNode === null || firstMergedNode === void 0 ? void 0 : firstMergedNode.mergedNodeContent
|
|
1103
|
-
: lastMergedNode === null || lastMergedNode === void 0 ? void 0 : lastMergedNode.mergedNodeContent;
|
|
1104
|
-
// Insert inside a merged node only if the slice was open (openStart > 0) and there exists mergedNodeContent.
|
|
1105
|
-
// Then we only have to ensure the depth is at the right level, so say a fully open blockquote insert will
|
|
1106
|
-
// be merged at the lowest, paragraph level, instead of blockquote level.
|
|
1107
|
-
const mergeStartNode = endTokenDeleted && openStart > 0 && depth === openStart && mergeContent !== undefined;
|
|
1108
|
-
// Same as above, merge nodes manually if there exists an open slice with mergeable content.
|
|
1109
|
-
// Compared to deleting an end token however, the merged block node is set as deleted. This is due to
|
|
1110
|
-
// ProseMirror node semantics as start tokens are considered to contain the actual node itself.
|
|
1111
|
-
const mergeEndNode = startTokenDeleted && openEnd > 0 && depth === openEnd && mergeContent !== undefined;
|
|
1112
|
-
if (mergeStartNode || mergeEndNode) {
|
|
1113
|
-
// Just as a fun fact that I found out while debugging this. Inserting text at paragraph position wraps
|
|
1114
|
-
// it into a new paragraph(!). So that's why you always offset your positions to insert it _inside_
|
|
1115
|
-
// the paragraph.
|
|
1116
|
-
steps.push({
|
|
1117
|
-
type: 'merge-fragment',
|
|
1118
|
-
pos,
|
|
1119
|
-
mergePos: mergeStartNode ? nodeEnd - openStart : pos + openEnd,
|
|
1120
|
-
from,
|
|
1121
|
-
to,
|
|
1122
|
-
node,
|
|
1123
|
-
fragment: setFragmentAsInserted(mergeContent, createNewInsertAttrs(trackAttrs), schema),
|
|
1124
|
-
});
|
|
1125
|
-
// Okay this is a bit ridiculous but it's used to adjust the insert pos when track changes prevents deletions
|
|
1126
|
-
// of merged nodes & content, as just using mapped toA in that case isn't the same.
|
|
1127
|
-
// The calculation is a bit mysterious, I admit.
|
|
1128
|
-
// TODO delete/fix this?
|
|
1129
|
-
// 'should prevent replacing of blockquotes and break the slice into parts instead' test needs this
|
|
1130
|
-
// if (node.isText) {
|
|
1131
|
-
// mergedInsertPos = offsetPos - openEnd
|
|
1132
|
-
// }
|
|
1133
|
-
}
|
|
1134
|
-
else if (node.isText) {
|
|
1135
|
-
// Text deletion is handled even when the deletion doesn't completely wrap the text node
|
|
1136
|
-
// (which is basically the case most of the time)
|
|
1137
|
-
steps.push({
|
|
1138
|
-
type: 'delete-text',
|
|
1139
|
-
pos,
|
|
1140
|
-
from: Math.max(pos, from),
|
|
1141
|
-
to: Math.min(nodeEnd, to),
|
|
1142
|
-
node,
|
|
1143
|
-
});
|
|
1144
|
-
}
|
|
1145
|
-
else ;
|
|
1146
|
-
}
|
|
1147
|
-
else if (nodeCompletelyDeleted) {
|
|
1148
|
-
steps.push({
|
|
1149
|
-
type: 'delete-node',
|
|
1150
|
-
pos,
|
|
1151
|
-
nodeEnd,
|
|
1152
|
-
node,
|
|
1153
|
-
});
|
|
1154
|
-
}
|
|
1155
|
-
}
|
|
1156
|
-
});
|
|
1157
|
-
return {
|
|
1158
|
-
sliceWasSplit: !!(firstMergedNode || lastMergedNode),
|
|
1159
|
-
newSliceContent: updatedSliceNodes
|
|
1160
|
-
? prosemirrorModel.Fragment.fromArray(updatedSliceNodes)
|
|
1161
|
-
: insertSlice.content,
|
|
1162
|
-
steps,
|
|
1163
|
-
};
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
|
-
/*!
|
|
1167
|
-
* © 2021 Atypon Systems LLC
|
|
1168
|
-
*
|
|
1169
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
1170
|
-
* you may not use this file except in compliance with the License.
|
|
1171
|
-
* You may obtain a copy of the License at
|
|
1172
|
-
*
|
|
1173
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
1174
|
-
*
|
|
1175
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
1176
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
1177
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
1178
|
-
* See the License for the specific language governing permissions and
|
|
1179
|
-
* limitations under the License.
|
|
1180
|
-
*/
|
|
1181
|
-
function trackReplaceAroundStep(step, oldState, tr, newTr, attrs) {
|
|
1182
|
-
log.info('###### ReplaceAroundStep ######');
|
|
1183
|
-
// @ts-ignore
|
|
1184
|
-
const { from, to, gapFrom, gapTo, insert, slice, structure, } = step;
|
|
1185
|
-
// Invert the transaction step to prevent it from actually deleting or inserting anything
|
|
1186
|
-
const newStep = step.invert(oldState.doc);
|
|
1187
|
-
const stepResult = newTr.maybeStep(newStep);
|
|
1188
|
-
if (stepResult.failed) {
|
|
1189
|
-
log.error(`inverting ReplaceAroundStep failed: "${stepResult.failed}"`, newStep);
|
|
1190
|
-
return [];
|
|
1191
|
-
}
|
|
1192
|
-
const gap = oldState.doc.slice(gapFrom, gapTo);
|
|
1193
|
-
log.info('RETAINED GAP CONTENT', gap);
|
|
1194
|
-
// First apply the deleted range and update the insert slice to not include content that was deleted,
|
|
1195
|
-
// eg partial nodes in an open-ended slice
|
|
1196
|
-
const { sliceWasSplit, newSliceContent, steps: deleteSteps, } = deleteAndMergeSplitNodes(from, to, { start: gapFrom, end: gapTo }, newTr.doc, newTr, oldState.schema, attrs, slice);
|
|
1197
|
-
const steps = deleteSteps;
|
|
1198
|
-
log.info('TR: new steps after applying delete', [...newTr.steps]);
|
|
1199
|
-
log.info('DELETE STEPS: ', deleteSteps);
|
|
1200
|
-
// We only want to insert when there something inside the gap (actually would this be always true?)
|
|
1201
|
-
// or insert slice wasn't just start/end tokens (which we already merged inside deleteAndMergeSplitBlockNodes)
|
|
1202
|
-
// ^^answering above comment we could have meta node like(bibliography_item, contributor) will not have content at all,
|
|
1203
|
-
// and that case gap will be 0, for that will use updateMetaNode to indicate that we are going just to update that node
|
|
1204
|
-
if (gap.size > 0 ||
|
|
1205
|
-
(!structure && newSliceContent.size > 0) ||
|
|
1206
|
-
tr.getMeta(TrackChangesAction.updateMetaNode)) {
|
|
1207
|
-
log.info('newSliceContent', newSliceContent);
|
|
1208
|
-
// Since deleteAndMergeSplitBlockNodes modified the slice to not to contain any merged nodes,
|
|
1209
|
-
// the sides should be equal. TODO can they be other than 0?
|
|
1210
|
-
const openStart = slice.openStart !== slice.openEnd || newSliceContent.size === 0 ? 0 : slice.openStart;
|
|
1211
|
-
const openEnd = slice.openStart !== slice.openEnd || newSliceContent.size === 0 ? 0 : slice.openEnd;
|
|
1212
|
-
let insertedSlice = new prosemirrorModel.Slice(setFragmentAsInserted(newSliceContent, createNewInsertAttrs(attrs), oldState.schema), openStart, openEnd);
|
|
1213
|
-
if (gap.size > 0 || tr.getMeta(TrackChangesAction.updateMetaNode)) {
|
|
1214
|
-
log.info('insertedSlice before inserted gap', insertedSlice);
|
|
1215
|
-
insertedSlice = insertedSlice.insertAt(insertedSlice.size === 0 ? 0 : insert, gap.content);
|
|
1216
|
-
log.info('insertedSlice after inserted gap', insertedSlice);
|
|
1217
|
-
}
|
|
1218
|
-
deleteSteps.push({
|
|
1219
|
-
type: 'insert-slice',
|
|
1220
|
-
from: gapFrom,
|
|
1221
|
-
to: gapTo,
|
|
1222
|
-
slice: insertedSlice,
|
|
1223
|
-
sliceWasSplit,
|
|
1224
|
-
});
|
|
1225
|
-
}
|
|
1226
|
-
return steps;
|
|
1227
|
-
}
|
|
1228
|
-
|
|
1229
|
-
/*!
|
|
1230
|
-
* © 2021 Atypon Systems LLC
|
|
1231
|
-
*
|
|
1232
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
1233
|
-
* you may not use this file except in compliance with the License.
|
|
1234
|
-
* You may obtain a copy of the License at
|
|
1235
|
-
*
|
|
1236
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
1237
|
-
*
|
|
1238
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
1239
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
1240
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
1241
|
-
* See the License for the specific language governing permissions and
|
|
1242
|
-
* limitations under the License.
|
|
1243
|
-
*/
|
|
1244
|
-
function trackReplaceStep(step, oldState, newTr, attrs, stepResult, currentStepDoc) {
|
|
1245
|
-
log.info('###### ReplaceStep ######');
|
|
1246
|
-
let selectionPos = 0;
|
|
1247
|
-
const changeSteps = [];
|
|
1248
|
-
// Invert the transaction step to prevent it from actually deleting or inserting anything
|
|
1249
|
-
step.getMap().forEach((fromA, toA, fromB, toB) => {
|
|
1250
|
-
var _a, _b;
|
|
1251
|
-
log.info(`changed ranges: ${fromA} ${toA} ${fromB} ${toB}`);
|
|
1252
|
-
const { slice } = step;
|
|
1253
|
-
log.info('TR: steps before applying delete', [...newTr.steps]);
|
|
1254
|
-
// First apply the deleted range and update the insert slice to not include content that was deleted,
|
|
1255
|
-
// eg partial nodes in an open-ended slice
|
|
1256
|
-
if (stepResult.failed) {
|
|
1257
|
-
log.error(`invert ReplaceStep failed: "${stepResult.failed}"`);
|
|
1258
|
-
return;
|
|
1259
|
-
}
|
|
1260
|
-
const { sliceWasSplit, newSliceContent, steps: deleteSteps, } = deleteAndMergeSplitNodes(fromA, toA, undefined, currentStepDoc, newTr, oldState.schema, attrs, slice);
|
|
1261
|
-
changeSteps.push(...deleteSteps);
|
|
1262
|
-
log.info('TR: steps after applying delete', [...newTr.steps]);
|
|
1263
|
-
log.info('DELETE STEPS: ', changeSteps);
|
|
1264
|
-
// console.log('CHANGE STEPS AT THIS POINT:')
|
|
1265
|
-
// console.log(JSON.parse(JSON.stringify(changeSteps)))
|
|
1266
|
-
function sameThingBackSpaced() {
|
|
1267
|
-
/*
|
|
1268
|
-
When deleting text with backspace and getting to the point of when a space and a character before a deleted piece of text is deleted
|
|
1269
|
-
the prosemirror would interpret it as moving the <del> node (this is a tracked deletion) one characted behind.
|
|
1270
|
-
It normally results in [delete, delete, insert] set of ChangSteps where the 1st delete is for the delete done by
|
|
1271
|
-
the backspace key, the second delete and the insert are a misinterpretation of the moved text. So these last 2 steps have to be caught
|
|
1272
|
-
and removed as they are not meaningful.
|
|
1273
|
-
*/
|
|
1274
|
-
if (changeSteps.length == 2 && newSliceContent.size > 0) {
|
|
1275
|
-
const correspondingDeletion = changeSteps.find(
|
|
1276
|
-
// @ts-ignore
|
|
1277
|
-
(step) => step.node.text === newSliceContent.content[0].text // @TODO - get more precise proof of match. E.g.: position approximation
|
|
1278
|
-
);
|
|
1279
|
-
return correspondingDeletion;
|
|
1280
|
-
}
|
|
1281
|
-
return undefined;
|
|
1282
|
-
}
|
|
1283
|
-
const backSpacedText = sameThingBackSpaced();
|
|
1284
|
-
if (backSpacedText) {
|
|
1285
|
-
changeSteps.splice(changeSteps.indexOf(backSpacedText));
|
|
1286
|
-
}
|
|
1287
|
-
const textWasDeleted = !!changeSteps.length;
|
|
1288
|
-
if (!backSpacedText && newSliceContent.size > 0) {
|
|
1289
|
-
log.info('newSliceContent', newSliceContent);
|
|
1290
|
-
// Since deleteAndMergeSplitBlockNodes modified the slice to not to contain any merged nodes,
|
|
1291
|
-
// the sides should be equal. TODO can they be other than 0?
|
|
1292
|
-
const openStart = slice.openStart !== slice.openEnd ? 0 : slice.openStart;
|
|
1293
|
-
const openEnd = slice.openStart !== slice.openEnd ? 0 : slice.openEnd;
|
|
1294
|
-
changeSteps.push({
|
|
1295
|
-
type: 'insert-slice',
|
|
1296
|
-
from: textWasDeleted ? fromB : toA,
|
|
1297
|
-
to: textWasDeleted ? toB - 1 : toA,
|
|
1298
|
-
sliceWasSplit,
|
|
1299
|
-
slice: new prosemirrorModel.Slice(setFragmentAsInserted(newSliceContent, createNewInsertAttrs(attrs), oldState.schema), openStart, openEnd),
|
|
1300
|
-
});
|
|
1301
|
-
}
|
|
1302
|
-
else {
|
|
1303
|
-
// Incase only deletion was applied, check whether tracked marks around deleted content can be merged
|
|
1304
|
-
// mergeTrackedMarks(adjustedInsertPos, newTr.doc, newTr, oldState.schema)
|
|
1305
|
-
// When DEL is used, the selection is set to the end of the deleted content
|
|
1306
|
-
// TODO: 'window.event' is deprecated, find a better way to detect the key used for deletion
|
|
1307
|
-
// @ts-ignore
|
|
1308
|
-
selectionPos = ((_a = window.event) === null || _a === void 0 ? void 0 : _a.code) === 'Delete' || ((_b = window.event) === null || _b === void 0 ? void 0 : _b.inputType) === 'deleteContentForward' ? toA : fromA;
|
|
1309
|
-
}
|
|
1310
|
-
});
|
|
1311
|
-
return [changeSteps, selectionPos];
|
|
1312
|
-
}
|
|
1313
|
-
|
|
1314
|
-
/*!
|
|
1315
|
-
* © 2021 Atypon Systems LLC
|
|
1316
|
-
*
|
|
1317
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
1318
|
-
* you may not use this file except in compliance with the License.
|
|
1319
|
-
* You may obtain a copy of the License at
|
|
1320
|
-
*
|
|
1321
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
1322
|
-
*
|
|
1323
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
1324
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
1325
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
1326
|
-
* See the License for the specific language governing permissions and
|
|
1327
|
-
* limitations under the License.
|
|
1328
|
-
*/
|
|
1329
|
-
/**
|
|
1330
|
-
* Deletes inserted text directly, otherwise wraps it with tracked_delete mark
|
|
1331
|
-
*
|
|
1332
|
-
* This would work for general inline nodes too, but since node marks don't work properly
|
|
1333
|
-
* with Yjs, attributes are used instead.
|
|
1334
|
-
* @param node
|
|
1335
|
-
* @param pos
|
|
1336
|
-
* @param newTr
|
|
1337
|
-
* @param schema
|
|
1338
|
-
* @param deleteAttrs
|
|
1339
|
-
* @param from
|
|
1340
|
-
* @param to
|
|
1341
|
-
* @returns position at the end of the possibly deleted text
|
|
1342
|
-
*/
|
|
1343
|
-
function deleteTextIfInserted(node, pos, newTr, schema, deleteAttrs, from, to) {
|
|
1344
|
-
const start = from ? Math.max(pos, from) : pos;
|
|
1345
|
-
const nodeEnd = pos + node.nodeSize;
|
|
1346
|
-
const end = to ? Math.min(nodeEnd, to) : nodeEnd;
|
|
1347
|
-
if (node.marks.find((m) => m.type === schema.marks.tracked_insert)) {
|
|
1348
|
-
// Math.max(pos, from) is for picking always the start of the node,
|
|
1349
|
-
// not the start of the change (which might span multiple nodes).
|
|
1350
|
-
// Pos can be less than from as nodesBetween iterates through all nodes starting from the top block node
|
|
1351
|
-
newTr.replaceWith(start, end, prosemirrorModel.Fragment.empty);
|
|
1352
|
-
return start;
|
|
1353
|
-
}
|
|
1354
|
-
else {
|
|
1355
|
-
const leftNode = newTr.doc.resolve(start).nodeBefore;
|
|
1356
|
-
const leftMarks = getMergeableMarkTrackedAttrs(leftNode, deleteAttrs, schema);
|
|
1357
|
-
const rightNode = newTr.doc.resolve(end).nodeAfter;
|
|
1358
|
-
const rightMarks = getMergeableMarkTrackedAttrs(rightNode, deleteAttrs, schema);
|
|
1359
|
-
const fromStartOfMark = start - (leftNode && leftMarks ? leftNode.nodeSize : 0);
|
|
1360
|
-
const toEndOfMark = end + (rightNode && rightMarks ? rightNode.nodeSize : 0);
|
|
1361
|
-
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);
|
|
1362
|
-
const dataTracked = addTrackIdIfDoesntExist({
|
|
1363
|
-
...leftMarks,
|
|
1364
|
-
...rightMarks,
|
|
1365
|
-
...deleteAttrs,
|
|
1366
|
-
createdAt,
|
|
1367
|
-
});
|
|
1368
|
-
newTr.addMark(fromStartOfMark, toEndOfMark, schema.marks.tracked_delete.create({
|
|
1369
|
-
dataTracked,
|
|
1370
|
-
}));
|
|
1371
|
-
return toEndOfMark;
|
|
1372
|
-
}
|
|
1373
|
-
}
|
|
1374
|
-
|
|
1375
|
-
/**
|
|
1376
|
-
* Merges tracked marks between text nodes at a position
|
|
1377
|
-
*
|
|
1378
|
-
* Will work for any nodes that use tracked_insert or tracked_delete marks which may not be preferrable
|
|
1379
|
-
* if used for block nodes (since we possibly want to show the individual changed nodes).
|
|
1380
|
-
* Merging is done based on the userID, operation type and status.
|
|
1381
|
-
* @param pos
|
|
1382
|
-
* @param doc
|
|
1383
|
-
* @param newTr
|
|
1384
|
-
* @param schema
|
|
1385
|
-
*/
|
|
1386
|
-
function mergeTrackedMarks(pos, doc, newTr, schema) {
|
|
1387
|
-
const resolved = doc.resolve(pos);
|
|
1388
|
-
const { nodeAfter, nodeBefore } = resolved;
|
|
1389
|
-
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];
|
|
1390
|
-
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];
|
|
1391
|
-
if (!nodeAfter || !nodeBefore || !leftMark || !rightMark || leftMark.type !== rightMark.type) {
|
|
1392
|
-
return;
|
|
1393
|
-
}
|
|
1394
|
-
const leftDataTracked = leftMark.attrs.dataTracked;
|
|
1395
|
-
const rightDataTracked = rightMark.attrs.dataTracked;
|
|
1396
|
-
if (!shouldMergeTrackedAttributes(leftDataTracked, rightDataTracked)) {
|
|
1397
|
-
return;
|
|
1398
|
-
}
|
|
1399
|
-
const isLeftOlder = (leftDataTracked.createdAt || 0) < (rightDataTracked.createdAt || 0);
|
|
1400
|
-
const ancestorAttrs = isLeftOlder ? leftDataTracked : rightDataTracked;
|
|
1401
|
-
const dataTracked = {
|
|
1402
|
-
...ancestorAttrs,
|
|
1403
|
-
updatedAt: Date.now(),
|
|
1404
|
-
};
|
|
1405
|
-
const fromStartOfMark = pos - nodeBefore.nodeSize;
|
|
1406
|
-
const toEndOfMark = pos + nodeAfter.nodeSize;
|
|
1407
|
-
newTr.addMark(fromStartOfMark, toEndOfMark, leftMark.type.create({ ...leftMark.attrs, dataTracked }));
|
|
1408
|
-
}
|
|
1409
|
-
|
|
1410
|
-
function processChangeSteps(changes, startPos, newTr, emptyAttrs, schema) {
|
|
1411
|
-
const mapping = new prosemirrorTransform.Mapping();
|
|
1412
|
-
const deleteAttrs = createNewDeleteAttrs(emptyAttrs);
|
|
1413
|
-
let selectionPos = startPos;
|
|
1414
|
-
// @TODO add custom handler / condition?
|
|
1415
|
-
changes.forEach((c) => {
|
|
1416
|
-
let step = newTr.steps[newTr.steps.length - 1];
|
|
1417
|
-
log.info('process change: ', c);
|
|
1418
|
-
// const handled = customStepHandler(changes, newTr, emptyAttrs) // ChangeStep[] | undefined
|
|
1419
|
-
if (c.type === 'delete-node') {
|
|
1420
|
-
deleteOrSetNodeDeleted(c.node, mapping.map(c.pos), newTr, deleteAttrs);
|
|
1421
|
-
const newestStep = newTr.steps[newTr.steps.length - 1];
|
|
1422
|
-
if (step !== newestStep) {
|
|
1423
|
-
mapping.appendMap(newestStep.getMap());
|
|
1424
|
-
step = newestStep;
|
|
1425
|
-
}
|
|
1426
|
-
mergeTrackedMarks(mapping.map(c.pos), newTr.doc, newTr, schema);
|
|
1427
|
-
}
|
|
1428
|
-
else if (c.type === 'delete-text') {
|
|
1429
|
-
const node = newTr.doc.nodeAt(mapping.map(c.pos));
|
|
1430
|
-
if (!node) {
|
|
1431
|
-
log.error(`processChangeSteps: no text node found for text-change`, c);
|
|
1432
|
-
return;
|
|
1433
|
-
}
|
|
1434
|
-
const where = deleteTextIfInserted(node, mapping.map(c.pos), newTr, schema, deleteAttrs, mapping.map(c.from), mapping.map(c.to));
|
|
1435
|
-
mergeTrackedMarks(where, newTr.doc, newTr, schema);
|
|
1436
|
-
}
|
|
1437
|
-
else if (c.type === 'merge-fragment') {
|
|
1438
|
-
let insertPos = mapping.map(c.mergePos);
|
|
1439
|
-
// The default insert position for block nodes is either the start of the merged content or the end.
|
|
1440
|
-
// Incase text was merged, this must be updated as the start or end of the node doesn't map to the
|
|
1441
|
-
// actual position of the merge. Currently the inserted content is inserted at the start or end
|
|
1442
|
-
// of the merged content, TODO reverse the start/end when end/start token?
|
|
1443
|
-
if (c.node.isText) {
|
|
1444
|
-
// When merging text we must delete text in the same go as well, as the from/to boundary goes through
|
|
1445
|
-
// the text node.
|
|
1446
|
-
insertPos = deleteTextIfInserted(c.node, mapping.map(c.pos), newTr, schema, deleteAttrs, mapping.map(c.from), mapping.map(c.to));
|
|
1447
|
-
const newestStep = newTr.steps[newTr.steps.length - 1];
|
|
1448
|
-
if (step !== newestStep) {
|
|
1449
|
-
mapping.appendMap(newestStep.getMap());
|
|
1450
|
-
step = newestStep;
|
|
1451
|
-
}
|
|
1452
|
-
}
|
|
1453
|
-
if (c.fragment.size > 0) {
|
|
1454
|
-
newTr.insert(insertPos, c.fragment);
|
|
1455
|
-
}
|
|
1456
|
-
}
|
|
1457
|
-
else if (c.type === 'insert-slice') {
|
|
1458
|
-
const newStep = new prosemirrorTransform.ReplaceStep(mapping.map(c.from), mapping.map(c.to), c.slice, false);
|
|
1459
|
-
const stepResult = newTr.maybeStep(newStep);
|
|
1460
|
-
if (stepResult.failed) {
|
|
1461
|
-
log.error(`processChangeSteps: insert-slice ReplaceStep failed "${stepResult.failed}"`, newStep);
|
|
1462
|
-
return;
|
|
1463
|
-
}
|
|
1464
|
-
mergeTrackedMarks(mapping.map(c.from), newTr.doc, newTr, schema);
|
|
1465
|
-
const to = mapping.map(c.to) + c.slice.size;
|
|
1466
|
-
mergeTrackedMarks(mapping.map(c.to) + (to < newTr.doc.nodeSize ? c.slice.size : 0), newTr.doc, newTr, schema);
|
|
1467
|
-
selectionPos = mapping.map(c.to) + c.slice.size;
|
|
1468
|
-
}
|
|
1469
|
-
else if (c.type === 'update-node-attrs') {
|
|
1470
|
-
const oldDataTracked = getBlockInlineTrackedData(c.node) || [];
|
|
1471
|
-
const oldUpdate = oldDataTracked.reverse().find((d) => {
|
|
1472
|
-
// reversing to start from the most recent change
|
|
1473
|
-
if (d.operation === exports.CHANGE_OPERATION.set_node_attributes &&
|
|
1474
|
-
(d.status === exports.CHANGE_STATUS.pending || d.status === exports.CHANGE_STATUS.rejected)) {
|
|
1475
|
-
return true;
|
|
1476
|
-
}
|
|
1477
|
-
return false;
|
|
1478
|
-
});
|
|
1479
|
-
// if the selected last change is with status "rejected" we need to use oldAttrs from it because
|
|
1480
|
-
// node's actual attributes represent the "rejected" values
|
|
1481
|
-
const lastChangeRejected = oldUpdate && oldUpdate.status === exports.CHANGE_STATUS.rejected;
|
|
1482
|
-
const sourceAttrs = (oldUpdate === null || oldUpdate === void 0 ? void 0 : oldUpdate.oldAttrs) || c.node.attrs;
|
|
1483
|
-
const { dataTracked, ...restAttrs } = sourceAttrs;
|
|
1484
|
-
const oldAttrs = lastChangeRejected ? oldUpdate.oldAttrs : restAttrs;
|
|
1485
|
-
const newDataTracked = [
|
|
1486
|
-
...oldDataTracked.filter((d) => !oldUpdate || d.id !== oldUpdate.id || lastChangeRejected),
|
|
1487
|
-
];
|
|
1488
|
-
const newUpdate = oldUpdate && oldUpdate.status !== exports.CHANGE_STATUS.rejected
|
|
1489
|
-
? {
|
|
1490
|
-
...oldUpdate,
|
|
1491
|
-
updatedAt: emptyAttrs.updatedAt,
|
|
1492
|
-
}
|
|
1493
|
-
: addTrackIdIfDoesntExist(createNewUpdateAttrs(emptyAttrs, lastChangeRejected ? oldAttrs : c.node.attrs));
|
|
1494
|
-
// Dont add update changes if there exists already an insert change for this node
|
|
1495
|
-
if ((JSON.stringify(oldAttrs) !== JSON.stringify(c.newAttrs) ||
|
|
1496
|
-
c.node.type === c.node.type.schema.nodes.citation) &&
|
|
1497
|
-
!oldDataTracked.find((d) => d.operation === exports.CHANGE_OPERATION.insert && d.status === exports.CHANGE_STATUS.pending)) {
|
|
1498
|
-
newDataTracked.push(newUpdate);
|
|
1499
|
-
}
|
|
1500
|
-
newTr.setNodeMarkup(mapping.map(c.pos), undefined, {
|
|
1501
|
-
...c.newAttrs,
|
|
1502
|
-
dataTracked: newDataTracked.length > 0 ? newDataTracked : null,
|
|
1503
|
-
}, c.node.marks);
|
|
1504
|
-
}
|
|
1505
|
-
const newestStep = newTr.steps[newTr.steps.length - 1];
|
|
1506
|
-
if (step !== newestStep) {
|
|
1507
|
-
mapping.appendMap(newestStep.getMap());
|
|
1508
|
-
}
|
|
1509
|
-
});
|
|
1510
|
-
return [mapping, selectionPos];
|
|
1511
|
-
}
|
|
1512
|
-
|
|
1513
|
-
/**
|
|
1514
|
-
* Matches deleted-text recursively to inserted text
|
|
1515
|
-
*
|
|
1516
|
-
* This is needed as text containing various marks is split into multiple parts even though it's
|
|
1517
|
-
* continously deleted. Therefore, we need to find the next part if there is any and keep going until
|
|
1518
|
-
* we've reached the end of the deleted text or inserted content.
|
|
1519
|
-
* @param adjDeleted
|
|
1520
|
-
* @param insNode
|
|
1521
|
-
* @param offset
|
|
1522
|
-
* @param matchedDeleted
|
|
1523
|
-
* @param deleted
|
|
1524
|
-
* @returns
|
|
1525
|
-
*/
|
|
1526
|
-
function matchText(adjDeleted, insNode, offset, matchedDeleted, deleted) {
|
|
1527
|
-
const { pos, from, to, node: delNode } = adjDeleted;
|
|
1528
|
-
let j = offset, d = from - pos, maxSteps = to - Math.max(pos, from);
|
|
1529
|
-
// Match text inside the inserted text node to the deleted text node
|
|
1530
|
-
for (; maxSteps !== j && insNode.text[j] !== undefined && insNode.text[j] === delNode.text[d]; j += 1, d += 1) {
|
|
1531
|
-
matchedDeleted += 1;
|
|
1532
|
-
}
|
|
1533
|
-
// this is needed incase diffing tr.doc
|
|
1534
|
-
// deleted.push({
|
|
1535
|
-
// pos: pos,
|
|
1536
|
-
// type: 'update-node-attrs',
|
|
1537
|
-
// // Should check the attrs for equality in fixInconsistentChanges? to remove dataTracked completely
|
|
1538
|
-
// oldAttrs: adjDeleted.node.attrs || {},
|
|
1539
|
-
// newAttrs: child.attrs || {},
|
|
1540
|
-
// })
|
|
1541
|
-
deleted = deleted.filter((d) => d !== adjDeleted);
|
|
1542
|
-
if (maxSteps !== j) {
|
|
1543
|
-
deleted.push({
|
|
1544
|
-
pos,
|
|
1545
|
-
from: from + j - offset,
|
|
1546
|
-
to,
|
|
1547
|
-
type: 'delete-text',
|
|
1548
|
-
node: delNode,
|
|
1549
|
-
});
|
|
1550
|
-
return [matchedDeleted, deleted];
|
|
1551
|
-
}
|
|
1552
|
-
const nextTextDelete = deleted.find((d) => d.type === 'delete-text' && d.pos === to);
|
|
1553
|
-
if (nextTextDelete) {
|
|
1554
|
-
return matchText(nextTextDelete, insNode, j, matchedDeleted, deleted);
|
|
1555
|
-
}
|
|
1556
|
-
return [matchedDeleted, deleted];
|
|
1557
|
-
}
|
|
1558
|
-
/**
|
|
1559
|
-
* Matches deleted to inserted content and returns the first pos they differ and the updated
|
|
1560
|
-
* ChangeStep list.
|
|
1561
|
-
*
|
|
1562
|
-
* Based on https://github.com/ProseMirror/prosemirror-model/blob/master/src/diff.ts
|
|
1563
|
-
* @param matchedDeleted
|
|
1564
|
-
* @param deleted
|
|
1565
|
-
* @param inserted
|
|
1566
|
-
* @returns
|
|
1567
|
-
*/
|
|
1568
|
-
function matchInserted(matchedDeleted, deleted, inserted) {
|
|
1569
|
-
var _a;
|
|
1570
|
-
let matched = [matchedDeleted, deleted];
|
|
1571
|
-
for (let i = 0;; i += 1) {
|
|
1572
|
-
if (inserted.childCount === i) {
|
|
1573
|
-
return matched;
|
|
1574
|
-
}
|
|
1575
|
-
const insNode = inserted.child(i);
|
|
1576
|
-
// @ts-ignore
|
|
1577
|
-
const adjDeleted = matched[1].find((d) => (d.type === 'delete-text' && Math.max(d.pos, d.from) === matched[0]) ||
|
|
1578
|
-
(d.type === 'delete-node' && d.pos === matched[0]));
|
|
1579
|
-
if (insNode.type !== ((_a = adjDeleted === null || adjDeleted === void 0 ? void 0 : adjDeleted.node) === null || _a === void 0 ? void 0 : _a.type)) {
|
|
1580
|
-
return matched;
|
|
1581
|
-
}
|
|
1582
|
-
else if (insNode.isText && (adjDeleted === null || adjDeleted === void 0 ? void 0 : adjDeleted.node)) {
|
|
1583
|
-
matched = matchText(adjDeleted, insNode, 0, matched[0], matched[1]);
|
|
1584
|
-
continue;
|
|
1585
|
-
}
|
|
1586
|
-
else if (insNode.content.size > 0 || (adjDeleted === null || adjDeleted === void 0 ? void 0 : adjDeleted.node.content.size) > 0) {
|
|
1587
|
-
// Move the inDeleted inside the block/inline node's boundary
|
|
1588
|
-
matched = matchInserted(matched[0] + 1, matched[1].filter((d) => d !== adjDeleted), insNode.content);
|
|
1589
|
-
}
|
|
1590
|
-
else {
|
|
1591
|
-
matched = [matched[0] + insNode.nodeSize, matched[1].filter((d) => d !== adjDeleted)];
|
|
1592
|
-
}
|
|
1593
|
-
// Omit dataTracked
|
|
1594
|
-
const { dataTracked, ...newAttrs } = insNode.attrs || {};
|
|
1595
|
-
matched[1].push({
|
|
1596
|
-
pos: adjDeleted.pos,
|
|
1597
|
-
type: 'update-node-attrs',
|
|
1598
|
-
node: adjDeleted.node,
|
|
1599
|
-
newAttrs,
|
|
1600
|
-
});
|
|
1601
|
-
}
|
|
1602
|
-
}
|
|
1603
|
-
|
|
1604
|
-
/*!
|
|
1605
|
-
* © 2021 Atypon Systems LLC
|
|
1606
|
-
*
|
|
1607
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
1608
|
-
* you may not use this file except in compliance with the License.
|
|
1609
|
-
* You may obtain a copy of the License at
|
|
1610
|
-
*
|
|
1611
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
1612
|
-
*
|
|
1613
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
1614
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
1615
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
1616
|
-
* See the License for the specific language governing permissions and
|
|
1617
|
-
* limitations under the License.
|
|
1618
|
-
*/
|
|
1619
|
-
/**
|
|
1620
|
-
* Cuts a fragment similar to Fragment.cut but also removes the parent node.
|
|
1621
|
-
*
|
|
1622
|
-
* @param matched
|
|
1623
|
-
* @param deleted
|
|
1624
|
-
* @param content
|
|
1625
|
-
* @returns
|
|
1626
|
-
*/
|
|
1627
|
-
function cutFragment(matched, deleted, content) {
|
|
1628
|
-
const newContent = [];
|
|
1629
|
-
for (let i = 0; matched <= deleted && i < content.childCount; i += 1) {
|
|
1630
|
-
const child = content.child(i);
|
|
1631
|
-
if (!child.isText && child.content.size > 0) {
|
|
1632
|
-
const cut = cutFragment(matched + 1, deleted, child.content);
|
|
1633
|
-
matched = cut[0];
|
|
1634
|
-
newContent.push(...cut[1].content);
|
|
1635
|
-
}
|
|
1636
|
-
else if (child.isText && matched + child.nodeSize > deleted) {
|
|
1637
|
-
if (deleted - matched > 0) {
|
|
1638
|
-
newContent.push(child.cut(deleted - matched));
|
|
1639
|
-
}
|
|
1640
|
-
else {
|
|
1641
|
-
newContent.push(child);
|
|
1642
|
-
}
|
|
1643
|
-
matched = deleted + 1;
|
|
1644
|
-
}
|
|
1645
|
-
else {
|
|
1646
|
-
matched += child.nodeSize;
|
|
1647
|
-
}
|
|
1648
|
-
}
|
|
1649
|
-
return [matched, prosemirrorModel.Fragment.fromArray(newContent)];
|
|
1650
|
-
}
|
|
1651
|
-
function diffChangeSteps(deleted, inserted) {
|
|
1652
|
-
const updated = [];
|
|
1653
|
-
let updatedDeleted = [...deleted];
|
|
1654
|
-
inserted.forEach((ins) => {
|
|
1655
|
-
log.info('DIFF ins ', ins);
|
|
1656
|
-
//
|
|
1657
|
-
// @TODO this is a temporary workaround to prevent duplicated diffing between splitSliceIntoMergedParts and
|
|
1658
|
-
// matchInserted.
|
|
1659
|
-
//
|
|
1660
|
-
// As originally authored splitSliceIntoMergedParts splits open slices into their merged parts
|
|
1661
|
-
// leaving out the need to insert the possibly deleted nodes into the doc. However, as matchInserted now
|
|
1662
|
-
// traverses the deleted range checking it against the inserted slice this behaves quite in a same way
|
|
1663
|
-
// where the opened block nodes are traversed but left unmodified. With an openStart > 0 though the
|
|
1664
|
-
// node-attr-updates would additionally have to be filtered out in the processChangeSteps.
|
|
1665
|
-
//
|
|
1666
|
-
// The old logic is still left as it's as refactoring is painful and would probably break something and just
|
|
1667
|
-
// in general, take a lot of time. Therefore, this sliceWasSplit boolean is used to just skip diffing.
|
|
1668
|
-
if (ins.sliceWasSplit) {
|
|
1669
|
-
updated.push(ins);
|
|
1670
|
-
return;
|
|
1671
|
-
}
|
|
1672
|
-
// Start diffing from the start of the deleted range
|
|
1673
|
-
const deleteStart = updatedDeleted.reduce((acc, cur) => {
|
|
1674
|
-
if (cur.type === 'delete-node') {
|
|
1675
|
-
return Math.min(acc, cur.pos);
|
|
1676
|
-
}
|
|
1677
|
-
else if (cur.type === 'delete-text') {
|
|
1678
|
-
return Math.min(acc, cur.from);
|
|
1679
|
-
}
|
|
1680
|
-
return acc;
|
|
1681
|
-
}, Number.MAX_SAFE_INTEGER);
|
|
1682
|
-
const [matchedDeleted, updatedDel] = matchInserted(deleteStart, updatedDeleted, ins.slice.content);
|
|
1683
|
-
if (matchedDeleted === deleteStart) {
|
|
1684
|
-
updated.push(ins);
|
|
1685
|
-
return;
|
|
1686
|
-
}
|
|
1687
|
-
updatedDeleted = updatedDel;
|
|
1688
|
-
const [_, newInserted] = cutFragment(0, matchedDeleted - deleteStart, ins.slice.content);
|
|
1689
|
-
if (newInserted.size > 0) {
|
|
1690
|
-
updated.push({
|
|
1691
|
-
...ins,
|
|
1692
|
-
slice: new prosemirrorModel.Slice(newInserted, ins.slice.openStart, ins.slice.openEnd),
|
|
1693
|
-
});
|
|
1694
|
-
}
|
|
1695
|
-
});
|
|
1696
|
-
log.info('FINISH DIFF: ', [...updatedDeleted, ...updated]);
|
|
1697
|
-
return [...updatedDeleted, ...updated];
|
|
1698
|
-
}
|
|
1699
|
-
|
|
1700
|
-
/**
|
|
1701
|
-
* Retrieves a static property from Selection class instead of having to use direct imports
|
|
1702
|
-
*
|
|
1703
|
-
* This skips the direct dependency to prosemirror-state where multiple versions might cause conflicts
|
|
1704
|
-
* as the created instances might belong to different prosemirror-state import than one used in the editor.
|
|
1705
|
-
* @param sel
|
|
1706
|
-
* @returns
|
|
1707
|
-
*/
|
|
1708
|
-
const getSelectionStaticConstructor = (sel) => Object.getPrototypeOf(sel).constructor;
|
|
1709
|
-
const isHighlightMarkerNode = (node) => node && node.type === node.type.schema.nodes.highlight_marker;
|
|
1710
|
-
/**
|
|
1711
|
-
* Inverts transactions to wrap their contents/operations with track data instead
|
|
1712
|
-
*
|
|
1713
|
-
* The main function of track changes that holds the most complex parts of this whole library.
|
|
1714
|
-
* Takes in as arguments the data from appendTransaction to reapply it with the track marks/attributes.
|
|
1715
|
-
* We could prevent the initial transaction from being applied all together but since invert works just
|
|
1716
|
-
* as well and we can use the intermediate doc for checking which nodes are changed, it's not prevented.
|
|
1717
|
-
*
|
|
1718
|
-
*
|
|
1719
|
-
* @param tr Original transaction
|
|
1720
|
-
* @param oldState State before transaction
|
|
1721
|
-
* @param newTr Transaction created from the new editor state
|
|
1722
|
-
* @param authorID User id
|
|
1723
|
-
* @returns newTr that inverts the initial tr and applies track attributes/marks
|
|
1724
|
-
*/
|
|
1725
|
-
function trackTransaction(tr, oldState, newTr, authorID) {
|
|
1726
|
-
var _a, _b, _c, _d;
|
|
1727
|
-
const emptyAttrs = {
|
|
1728
|
-
authorID,
|
|
1729
|
-
reviewedByID: null,
|
|
1730
|
-
createdAt: tr.time,
|
|
1731
|
-
updatedAt: tr.time,
|
|
1732
|
-
status: exports.CHANGE_STATUS.pending,
|
|
1733
|
-
};
|
|
1734
|
-
// Must use constructor.name instead of instanceof as aliasing prosemirror-state is a lot more
|
|
1735
|
-
// difficult than prosemirror-transform
|
|
1736
|
-
const wasNodeSelection = tr.selection instanceof prosemirrorState.NodeSelection;
|
|
1737
|
-
let iters = 0;
|
|
1738
|
-
log.info('ORIGINAL transaction', tr);
|
|
1739
|
-
for (let i = tr.steps.length - 1; i >= 0; i--) {
|
|
1740
|
-
const step = tr.steps[i];
|
|
1741
|
-
log.info('transaction step', step);
|
|
1742
|
-
iters += 1;
|
|
1743
|
-
if (iters > 20) {
|
|
1744
|
-
console.error('@manuscripts/track-changes-plugin: Possible infinite loop in iterating tr.steps, tracking skipped!\n' +
|
|
1745
|
-
'This is probably an error with the library, please report back to maintainers with a reproduction if possible', newTr);
|
|
1746
|
-
continue;
|
|
1747
|
-
}
|
|
1748
|
-
else if (!(step instanceof prosemirrorTransform.ReplaceStep) && step.constructor.name === 'ReplaceStep') {
|
|
1749
|
-
console.error('@manuscripts/track-changes-plugin: Multiple prosemirror-transform packages imported, alias/dedupe them ' +
|
|
1750
|
-
'or instanceof checks fail as well as creating new steps');
|
|
1751
|
-
continue;
|
|
1752
|
-
}
|
|
1753
|
-
else if (step instanceof prosemirrorTransform.ReplaceStep) {
|
|
1754
|
-
const { slice } = step;
|
|
1755
|
-
if (((_b = (_a = slice === null || slice === void 0 ? void 0 : slice.content) === null || _a === void 0 ? void 0 : _a.content) === null || _b === void 0 ? void 0 : _b.length) === 1 &&
|
|
1756
|
-
isHighlightMarkerNode(slice.content.content[0])) {
|
|
1757
|
-
// don't track highlight marker nodes
|
|
1758
|
-
continue;
|
|
1759
|
-
}
|
|
1760
|
-
const newStep = step.invert(tr.docs[i]);
|
|
1761
|
-
const stepResult = newTr.maybeStep(newStep);
|
|
1762
|
-
let [steps, startPos] = trackReplaceStep(step, oldState, newTr, emptyAttrs, stepResult, tr.docs[i]);
|
|
1763
|
-
if (steps.length === 1) {
|
|
1764
|
-
const step = steps[0]; // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
1765
|
-
if (isHighlightMarkerNode((step === null || step === void 0 ? void 0 : step.node) || ((_d = (_c = step === null || step === void 0 ? void 0 : step.slice) === null || _c === void 0 ? void 0 : _c.content) === null || _d === void 0 ? void 0 : _d.content[0]))) {
|
|
1766
|
-
// don't track deleted highlight marker nodes
|
|
1767
|
-
continue;
|
|
1768
|
-
}
|
|
1769
|
-
}
|
|
1770
|
-
log.info('CHANGES: ', steps);
|
|
1771
|
-
// deleted and merged really...
|
|
1772
|
-
const deleted = steps.filter((s) => s.type !== 'insert-slice');
|
|
1773
|
-
const inserted = steps.filter((s) => s.type === 'insert-slice');
|
|
1774
|
-
steps = diffChangeSteps(deleted, inserted);
|
|
1775
|
-
log.info('DIFFED STEPS: ', steps);
|
|
1776
|
-
const [mapping, selectionPos] = processChangeSteps(steps, startPos || tr.selection.head, // Incase startPos is it's default value 0, use the old selection head
|
|
1777
|
-
newTr, emptyAttrs, oldState.schema);
|
|
1778
|
-
if (!wasNodeSelection) {
|
|
1779
|
-
const sel = getSelectionStaticConstructor(tr.selection);
|
|
1780
|
-
// Use Selection.near to fix selections that point to a block node instead of inline content
|
|
1781
|
-
// eg when inserting a complete new paragraph. -1 finds the first valid position moving backwards
|
|
1782
|
-
// inside the content
|
|
1783
|
-
const near = sel.near(newTr.doc.resolve(selectionPos), -1);
|
|
1784
|
-
newTr.setSelection(near);
|
|
1785
|
-
}
|
|
1786
|
-
}
|
|
1787
|
-
else if (step instanceof prosemirrorTransform.ReplaceAroundStep) {
|
|
1788
|
-
let steps = trackReplaceAroundStep(step, oldState, tr, newTr, emptyAttrs);
|
|
1789
|
-
const deleted = steps.filter((s) => s.type !== 'insert-slice');
|
|
1790
|
-
const inserted = steps.filter((s) => s.type === 'insert-slice');
|
|
1791
|
-
log.info('INSERT STEPS: ', inserted);
|
|
1792
|
-
steps = diffChangeSteps(deleted, inserted);
|
|
1793
|
-
log.info('DIFFED STEPS: ', steps);
|
|
1794
|
-
processChangeSteps(steps, tr.selection.from, newTr, emptyAttrs, oldState.schema);
|
|
1795
|
-
}
|
|
1796
|
-
// } else if (step instanceof AddMarkStep) {
|
|
1797
|
-
// } else if (step instanceof RemoveMarkStep) {
|
|
1798
|
-
// TODO: here we could check whether adjacent inserts & deletes cancel each other out.
|
|
1799
|
-
// However, this should not be done by diffing and only matching node or char by char instead since
|
|
1800
|
-
// it's A easier and B more intuitive to user.
|
|
1801
|
-
// The old meta keys are not copied to the new transaction since this will cause race-conditions
|
|
1802
|
-
// when a single meta-field is expected to having been processed / removed. Generic input meta keys,
|
|
1803
|
-
// inputType and uiEvent, are re-added since some plugins might depend on them and process the transaction
|
|
1804
|
-
// after track-changes plugin.
|
|
1805
|
-
tr.getMeta('inputType') && newTr.setMeta('inputType', tr.getMeta('inputType'));
|
|
1806
|
-
tr.getMeta('uiEvent') && newTr.setMeta('uiEvent', tr.getMeta('uiEvent'));
|
|
1807
|
-
}
|
|
1808
|
-
// This is kinda hacky solution at the moment to maintain NodeSelections over transactions
|
|
1809
|
-
// These are required by at least cross-references and links to activate their selector pop-ups
|
|
1810
|
-
if (wasNodeSelection) {
|
|
1811
|
-
console.log('%c Getting into node select! ', 'background: #222; color: #bada55');
|
|
1812
|
-
// And -1 here is necessary to keep the selection pointing at the start of the node
|
|
1813
|
-
// (or something, breaks with cross-references otherwise)
|
|
1814
|
-
const mappedPos = newTr.mapping.map(tr.selection.from, -1);
|
|
1815
|
-
const sel = getSelectionStaticConstructor(tr.selection);
|
|
1816
|
-
newTr.setSelection(sel.create(newTr.doc, mappedPos));
|
|
1817
|
-
}
|
|
1818
|
-
log.info('NEW transaction', newTr);
|
|
1819
|
-
return newTr;
|
|
1820
|
-
}
|
|
1821
|
-
|
|
1822
|
-
exports.TrackChangesStatus = void 0;
|
|
1823
|
-
(function (TrackChangesStatus) {
|
|
1824
|
-
TrackChangesStatus["enabled"] = "enabled";
|
|
1825
|
-
TrackChangesStatus["viewSnapshots"] = "view-snapshots";
|
|
1826
|
-
TrackChangesStatus["disabled"] = "disabled";
|
|
1827
|
-
})(exports.TrackChangesStatus || (exports.TrackChangesStatus = {}));
|
|
1828
|
-
|
|
1829
|
-
/*!
|
|
1830
|
-
* © 2021 Atypon Systems LLC
|
|
1831
|
-
*
|
|
1832
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
1833
|
-
* you may not use this file except in compliance with the License.
|
|
1834
|
-
* You may obtain a copy of the License at
|
|
1835
|
-
*
|
|
1836
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
1837
|
-
*
|
|
1838
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
1839
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
1840
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
1841
|
-
* See the License for the specific language governing permissions and
|
|
1842
|
-
* limitations under the License.
|
|
1843
|
-
*/
|
|
1844
|
-
const trackChangesPluginKey = new prosemirrorState.PluginKey('track-changes');
|
|
1845
|
-
/**
|
|
1846
|
-
* The ProseMirror plugin needed to enable track-changes.
|
|
1847
|
-
*
|
|
1848
|
-
* Accepts an empty options object as an argument but note that this uses 'anonymous:Anonymous' as the default userID.
|
|
1849
|
-
* @param opts
|
|
1850
|
-
*/
|
|
1851
|
-
const trackChangesPlugin = (opts = { userID: 'anonymous:Anonymous' }) => {
|
|
1852
|
-
const { userID, debug, skipTrsWithMetas = [] } = opts;
|
|
1853
|
-
let editorView;
|
|
1854
|
-
if (debug) {
|
|
1855
|
-
enableDebug(true);
|
|
1856
|
-
}
|
|
1857
|
-
return new prosemirrorState.Plugin({
|
|
1858
|
-
key: trackChangesPluginKey,
|
|
1859
|
-
props: {
|
|
1860
|
-
editable(state) {
|
|
1861
|
-
var _a;
|
|
1862
|
-
return ((_a = trackChangesPluginKey.getState(state)) === null || _a === void 0 ? void 0 : _a.status) !== exports.TrackChangesStatus.viewSnapshots;
|
|
1863
|
-
},
|
|
1864
|
-
},
|
|
1865
|
-
state: {
|
|
1866
|
-
init(_config, state) {
|
|
1867
|
-
return {
|
|
1868
|
-
status: exports.TrackChangesStatus.enabled,
|
|
1869
|
-
userID,
|
|
1870
|
-
changeSet: findChanges(state),
|
|
1871
|
-
};
|
|
1872
|
-
},
|
|
1873
|
-
apply(tr, pluginState, _oldState, newState) {
|
|
1874
|
-
const setUserID = getAction(tr, TrackChangesAction.setUserID);
|
|
1875
|
-
const setStatus = getAction(tr, TrackChangesAction.setPluginStatus);
|
|
1876
|
-
if (setUserID) {
|
|
1877
|
-
return { ...pluginState, userID: setUserID };
|
|
1878
|
-
}
|
|
1879
|
-
else if (setStatus) {
|
|
1880
|
-
return {
|
|
1881
|
-
...pluginState,
|
|
1882
|
-
status: setStatus,
|
|
1883
|
-
changeSet: setStatus === exports.TrackChangesStatus.disabled ? new ChangeSet() : findChanges(newState),
|
|
1884
|
-
};
|
|
1885
|
-
}
|
|
1886
|
-
else if (pluginState.status === exports.TrackChangesStatus.disabled) {
|
|
1887
|
-
return { ...pluginState, changeSet: new ChangeSet() };
|
|
1888
|
-
}
|
|
1889
|
-
let { changeSet, ...rest } = pluginState;
|
|
1890
|
-
if (getAction(tr, TrackChangesAction.refreshChanges)) {
|
|
1891
|
-
changeSet = findChanges(newState);
|
|
1892
|
-
}
|
|
1893
|
-
return {
|
|
1894
|
-
changeSet,
|
|
1895
|
-
...rest,
|
|
1896
|
-
};
|
|
1897
|
-
},
|
|
1898
|
-
},
|
|
1899
|
-
view(p) {
|
|
1900
|
-
editorView = p;
|
|
1901
|
-
return {
|
|
1902
|
-
update: undefined,
|
|
1903
|
-
destroy: undefined,
|
|
1904
|
-
};
|
|
1905
|
-
},
|
|
1906
|
-
appendTransaction(trs, oldState, newState) {
|
|
1907
|
-
const pluginState = trackChangesPluginKey.getState(newState);
|
|
1908
|
-
if (!pluginState || pluginState.status === exports.TrackChangesStatus.disabled || !(editorView === null || editorView === void 0 ? void 0 : editorView.editable)) {
|
|
1909
|
-
return null;
|
|
1910
|
-
}
|
|
1911
|
-
const { userID, changeSet } = pluginState;
|
|
1912
|
-
let createdTr = newState.tr, docChanged = false;
|
|
1913
|
-
log.info('TRS', trs);
|
|
1914
|
-
trs.forEach((tr) => {
|
|
1915
|
-
const wasAppended = tr.getMeta('appendedTransaction');
|
|
1916
|
-
const skipMetaUsed = skipTrsWithMetas.some((m) => tr.getMeta(m) || (wasAppended === null || wasAppended === void 0 ? void 0 : wasAppended.getMeta(m)));
|
|
1917
|
-
const skipTrackUsed = getAction(tr, TrackChangesAction.skipTrack) || (wasAppended && getAction(wasAppended, TrackChangesAction.skipTrack));
|
|
1918
|
-
if (tr.docChanged && !skipMetaUsed && !skipTrackUsed && !tr.getMeta('history$') && !(wasAppended && tr.getMeta('origin') === 'paragraphs')) {
|
|
1919
|
-
createdTr = trackTransaction(tr, oldState, createdTr, userID);
|
|
1920
|
-
}
|
|
1921
|
-
docChanged = docChanged || tr.docChanged;
|
|
1922
|
-
const setChangeStatuses = getAction(tr, TrackChangesAction.setChangeStatuses);
|
|
1923
|
-
if (setChangeStatuses) {
|
|
1924
|
-
const { status, ids } = setChangeStatuses;
|
|
1925
|
-
ids.forEach((changeId) => {
|
|
1926
|
-
const change = changeSet === null || changeSet === void 0 ? void 0 : changeSet.get(changeId);
|
|
1927
|
-
if (change) {
|
|
1928
|
-
createdTr = updateChangeAttrs(createdTr, change, { ...change.dataTracked, status, reviewedByID: userID }, oldState.schema);
|
|
1929
|
-
}
|
|
1930
|
-
});
|
|
1931
|
-
}
|
|
1932
|
-
else if (getAction(tr, TrackChangesAction.applyAndRemoveChanges)) {
|
|
1933
|
-
const mapping = applyAcceptedRejectedChanges(createdTr, oldState.schema, changeSet.bothNodeChanges);
|
|
1934
|
-
applyAcceptedRejectedChanges(createdTr, oldState.schema, changeSet.textChanges, mapping);
|
|
1935
|
-
setAction(createdTr, TrackChangesAction.refreshChanges, true);
|
|
1936
|
-
}
|
|
1937
|
-
});
|
|
1938
|
-
const changed = pluginState.changeSet.hasInconsistentData && fixInconsistentChanges(pluginState.changeSet, userID, createdTr, oldState.schema);
|
|
1939
|
-
if (changed) {
|
|
1940
|
-
log.warn('had to fix inconsistent changes in', createdTr);
|
|
1941
|
-
}
|
|
1942
|
-
if (docChanged || createdTr.docChanged || changed) {
|
|
1943
|
-
createdTr.setMeta('origin', trackChangesPluginKey);
|
|
1944
|
-
return setAction(createdTr, TrackChangesAction.refreshChanges, true);
|
|
1945
|
-
}
|
|
1946
|
-
return null;
|
|
1947
|
-
},
|
|
1948
|
-
});
|
|
1949
|
-
};
|
|
1950
|
-
|
|
1951
|
-
/**
|
|
1952
|
-
* Sets track-changes plugin's status to any of: 'enabled' 'disabled' 'viewSnapshots'. Passing undefined will
|
|
1953
|
-
* set 'enabled' status to 'disabled' and 'disabled' | 'viewSnapshots' status to 'enabled'.
|
|
1954
|
-
*
|
|
1955
|
-
* In disabled view, the plugin is completely inactive and changes are not updated anymore.
|
|
1956
|
-
* In viewSnasphots state, editor is set uneditable by editable prop that allows only selection changes
|
|
1957
|
-
* to the document.
|
|
1958
|
-
* @param status
|
|
1959
|
-
*/
|
|
1960
|
-
const setTrackingStatus = (status) => (state, dispatch) => {
|
|
1961
|
-
var _a;
|
|
1962
|
-
const currentStatus = (_a = trackChangesPluginKey.getState(state)) === null || _a === void 0 ? void 0 : _a.status;
|
|
1963
|
-
if (currentStatus) {
|
|
1964
|
-
let newStatus = status;
|
|
1965
|
-
if (newStatus === undefined) {
|
|
1966
|
-
newStatus =
|
|
1967
|
-
currentStatus === exports.TrackChangesStatus.enabled
|
|
1968
|
-
? exports.TrackChangesStatus.disabled
|
|
1969
|
-
: exports.TrackChangesStatus.enabled;
|
|
1970
|
-
}
|
|
1971
|
-
dispatch && dispatch(setAction(state.tr, TrackChangesAction.setPluginStatus, newStatus));
|
|
1972
|
-
return true;
|
|
1973
|
-
}
|
|
1974
|
-
return false;
|
|
1975
|
-
};
|
|
1976
|
-
/**
|
|
1977
|
-
* Appends a transaction to set change attributes/marks' statuses to any of: 'pending' 'accepted' 'rejected'.
|
|
1978
|
-
* @param status
|
|
1979
|
-
* @param ids
|
|
1980
|
-
*/
|
|
1981
|
-
const setChangeStatuses = (status, ids) => (state, dispatch) => {
|
|
1982
|
-
dispatch &&
|
|
1983
|
-
dispatch(setAction(state.tr, TrackChangesAction.setChangeStatuses, {
|
|
1984
|
-
status,
|
|
1985
|
-
ids,
|
|
1986
|
-
}));
|
|
1987
|
-
return true;
|
|
1988
|
-
};
|
|
1989
|
-
/**
|
|
1990
|
-
* Sets track-changes plugin's userID.
|
|
1991
|
-
* @param userID
|
|
1992
|
-
*/
|
|
1993
|
-
const setUserID = (userID) => (state, dispatch) => {
|
|
1994
|
-
dispatch && dispatch(setAction(state.tr, TrackChangesAction.setUserID, userID));
|
|
1995
|
-
return true;
|
|
1996
|
-
};
|
|
1997
|
-
/**
|
|
1998
|
-
* Appends a transaction that applies all 'accepted' and 'rejected' changes to the document.
|
|
1999
|
-
*/
|
|
2000
|
-
const applyAndRemoveChanges = () => (state, dispatch) => {
|
|
2001
|
-
dispatch && dispatch(setAction(state.tr, TrackChangesAction.applyAndRemoveChanges, true));
|
|
2002
|
-
return true;
|
|
2003
|
-
};
|
|
2004
|
-
/**
|
|
2005
|
-
* Runs `findChanges` to iterate over the document to collect changes into a new ChangeSet.
|
|
2006
|
-
*/
|
|
2007
|
-
const refreshChanges = () => (state, dispatch) => {
|
|
2008
|
-
dispatch && dispatch(setAction(state.tr, TrackChangesAction.refreshChanges, true));
|
|
2009
|
-
return true;
|
|
2010
|
-
};
|
|
2011
|
-
|
|
2012
|
-
var commands = /*#__PURE__*/Object.freeze({
|
|
2013
|
-
__proto__: null,
|
|
2014
|
-
setTrackingStatus: setTrackingStatus,
|
|
2015
|
-
setChangeStatuses: setChangeStatuses,
|
|
2016
|
-
setUserID: setUserID,
|
|
2017
|
-
applyAndRemoveChanges: applyAndRemoveChanges,
|
|
2018
|
-
refreshChanges: refreshChanges
|
|
2019
|
-
});
|
|
2020
|
-
|
|
2021
|
-
exports.ChangeSet = ChangeSet;
|
|
2022
|
-
exports.enableDebug = enableDebug;
|
|
2023
|
-
exports.skipTracking = skipTracking;
|
|
2024
|
-
exports.trackChangesPlugin = trackChangesPlugin;
|
|
2025
|
-
exports.trackChangesPluginKey = trackChangesPluginKey;
|
|
2026
|
-
exports.trackCommands = commands;
|