@lofcz/platejs-suggestion 52.0.11
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/LICENSE +24 -0
- package/README.md +5 -0
- package/dist/index.d.ts +218 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/react/index.d.ts +6 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +10 -0
- package/dist/react/index.js.map +1 -0
- package/dist/src-COX5sId2.js +914 -0
- package/dist/src-COX5sId2.js.map +1 -0
- package/package.json +60 -0
|
@@ -0,0 +1,914 @@
|
|
|
1
|
+
import { ElementApi, KEYS, NodeApi, PathApi, PointApi, TextApi, combineMatchOptions, createTSlatePlugin, getAt, isDefined, nanoid } from "platejs";
|
|
2
|
+
import { computeDiff } from "@platejs/diff";
|
|
3
|
+
|
|
4
|
+
//#region src/lib/utils/SkipSuggestionDeletes.ts
|
|
5
|
+
/**
|
|
6
|
+
* Recursively extracts text content from a node tree, excluding any text marked
|
|
7
|
+
* with "remove" suggestions. but include the text marked with "insert" and
|
|
8
|
+
* "update" suggestions.
|
|
9
|
+
*/
|
|
10
|
+
const SkipSuggestionDeletes = (editor, node) => {
|
|
11
|
+
if (TextApi.isText(node) || ElementApi.isElement(node) && editor.api.isInline(node)) {
|
|
12
|
+
if (ElementApi.isElement(node)) return NodeApi.string(node);
|
|
13
|
+
if (!node[KEYS.suggestion]) return node.text;
|
|
14
|
+
if (editor.getApi(BaseSuggestionPlugin).suggestion.suggestionData(node)?.type === "remove") return "";
|
|
15
|
+
return node.text;
|
|
16
|
+
}
|
|
17
|
+
return node.children.map((child) => SkipSuggestionDeletes(editor, child)).join("");
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/lib/utils/getSuggestionId.ts
|
|
22
|
+
const getSuggestionKeyId = (node) => {
|
|
23
|
+
return Object.keys(node).filter((key) => key.startsWith(`${KEYS.suggestion}_`)).at(-1);
|
|
24
|
+
};
|
|
25
|
+
const getInlineSuggestionData = (node) => {
|
|
26
|
+
const keyId = getSuggestionKeyId(node);
|
|
27
|
+
if (!keyId) return;
|
|
28
|
+
return node[keyId];
|
|
29
|
+
};
|
|
30
|
+
const keyId2SuggestionId = (keyId) => keyId.replace(`${KEYS.suggestion}_`, "");
|
|
31
|
+
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region src/lib/utils/getSuggestionKeys.ts
|
|
34
|
+
const getSuggestionKey = (id = "0") => `${KEYS.suggestion}_${id}`;
|
|
35
|
+
const isSuggestionKey = (key) => key.startsWith(`${KEYS.suggestion}_`);
|
|
36
|
+
const getSuggestionKeys = (node) => {
|
|
37
|
+
const keys = [];
|
|
38
|
+
Object.keys(node).forEach((key) => {
|
|
39
|
+
if (isSuggestionKey(key)) keys.push(key);
|
|
40
|
+
});
|
|
41
|
+
return keys;
|
|
42
|
+
};
|
|
43
|
+
const getSuggestionUserIdByKey = (key) => isDefined(key) ? key.split(`${KEYS.suggestion}_`)[1] : null;
|
|
44
|
+
const getSuggestionUserIds = (node) => getSuggestionKeys(node).map((key) => getSuggestionUserIdByKey(key));
|
|
45
|
+
const getSuggestionUserId = (node) => getSuggestionUserIds(node)[0];
|
|
46
|
+
const isCurrentUserSuggestion = (editor, node) => {
|
|
47
|
+
const { currentUserId } = editor.getOptions(BaseSuggestionPlugin);
|
|
48
|
+
return getInlineSuggestionData(node)?.userId === currentUserId;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
//#endregion
|
|
52
|
+
//#region src/lib/utils/getSuggestionNodeEntries.ts
|
|
53
|
+
const getSuggestionNodeEntries = (editor, suggestionId, { at = [], ...options } = {}) => editor.api.nodes({
|
|
54
|
+
at,
|
|
55
|
+
...options,
|
|
56
|
+
match: combineMatchOptions(editor, (n) => n.suggestionId === suggestionId, options)
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
//#endregion
|
|
60
|
+
//#region src/lib/utils/getActiveSuggestionDescriptions.ts
|
|
61
|
+
/**
|
|
62
|
+
* Get the suggestion descriptions of the selected node. A node can have
|
|
63
|
+
* multiple suggestions (multiple users). Each description maps to a user
|
|
64
|
+
* suggestion.
|
|
65
|
+
*/
|
|
66
|
+
const getActiveSuggestionDescriptions = (editor) => {
|
|
67
|
+
const aboveEntry = editor.getApi(BaseSuggestionPlugin).suggestion.node({ isText: true });
|
|
68
|
+
if (!aboveEntry) return [];
|
|
69
|
+
const aboveNode = aboveEntry[0];
|
|
70
|
+
const suggestionId = editor.getApi(BaseSuggestionPlugin).suggestion.nodeId(aboveNode);
|
|
71
|
+
if (!suggestionId) return [];
|
|
72
|
+
return getSuggestionUserIds(aboveNode).map((userId) => {
|
|
73
|
+
const nodes = Array.from(getSuggestionNodeEntries(editor, suggestionId, { match: (n) => n[getSuggestionKey(userId)] })).map(([node]) => node);
|
|
74
|
+
const insertions = nodes.filter((node) => !node.suggestionDeletion);
|
|
75
|
+
const deletions = nodes.filter((node) => node.suggestionDeletion);
|
|
76
|
+
const insertedText = insertions.map((node) => node.text).join("");
|
|
77
|
+
const deletedText = deletions.map((node) => node.text).join("");
|
|
78
|
+
if (insertions.length > 0 && deletions.length > 0) return {
|
|
79
|
+
deletedText,
|
|
80
|
+
insertedText,
|
|
81
|
+
suggestionId,
|
|
82
|
+
type: "replacement",
|
|
83
|
+
userId
|
|
84
|
+
};
|
|
85
|
+
if (deletions.length > 0) return {
|
|
86
|
+
deletedText,
|
|
87
|
+
suggestionId,
|
|
88
|
+
type: "deletion",
|
|
89
|
+
userId
|
|
90
|
+
};
|
|
91
|
+
return {
|
|
92
|
+
insertedText,
|
|
93
|
+
suggestionId,
|
|
94
|
+
type: "insertion",
|
|
95
|
+
userId
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
//#endregion
|
|
101
|
+
//#region src/lib/utils/getTransientSuggestionKey.ts
|
|
102
|
+
const getTransientSuggestionKey = () => `${KEYS.suggestion}Transient`;
|
|
103
|
+
|
|
104
|
+
//#endregion
|
|
105
|
+
//#region src/lib/queries/findSuggestionNode.ts
|
|
106
|
+
const findInlineSuggestionNode = (editor, options = {}) => editor.api.node({
|
|
107
|
+
...options,
|
|
108
|
+
match: combineMatchOptions(editor, (n) => TextApi.isText(n) && n[KEYS.suggestion], options)
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
//#endregion
|
|
112
|
+
//#region src/lib/queries/findSuggestionProps.ts
|
|
113
|
+
const findSuggestionProps = (editor, { at, type }) => {
|
|
114
|
+
const defaultProps = {
|
|
115
|
+
id: nanoid(),
|
|
116
|
+
createdAt: Date.now()
|
|
117
|
+
};
|
|
118
|
+
const api = editor.getApi(BaseSuggestionPlugin);
|
|
119
|
+
let entry = api.suggestion.node({
|
|
120
|
+
at,
|
|
121
|
+
isText: true
|
|
122
|
+
});
|
|
123
|
+
if (!entry) {
|
|
124
|
+
let start;
|
|
125
|
+
let end;
|
|
126
|
+
try {
|
|
127
|
+
[start, end] = editor.api.edges(at);
|
|
128
|
+
} catch {
|
|
129
|
+
return defaultProps;
|
|
130
|
+
}
|
|
131
|
+
const nextPoint = editor.api.after(end);
|
|
132
|
+
if (nextPoint) {
|
|
133
|
+
entry = api.suggestion.node({
|
|
134
|
+
at: nextPoint,
|
|
135
|
+
isText: true
|
|
136
|
+
});
|
|
137
|
+
if (!entry) {
|
|
138
|
+
const prevPoint = editor.api.before(start);
|
|
139
|
+
if (prevPoint) entry = api.suggestion.node({
|
|
140
|
+
at: prevPoint,
|
|
141
|
+
isText: true
|
|
142
|
+
});
|
|
143
|
+
if (!entry && editor.api.isStart(start, at)) {
|
|
144
|
+
const _at = prevPoint ?? at;
|
|
145
|
+
const lineBreakData = editor.api.above({ at: _at })?.[0].suggestion;
|
|
146
|
+
if (lineBreakData?.isLineBreak) return {
|
|
147
|
+
id: lineBreakData?.id ?? nanoid(),
|
|
148
|
+
createdAt: lineBreakData?.createdAt ?? Date.now()
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (entry && getInlineSuggestionData(entry[0])?.type === type && isCurrentUserSuggestion(editor, entry[0])) return {
|
|
155
|
+
id: api.suggestion.nodeId(entry[0]) ?? nanoid(),
|
|
156
|
+
createdAt: getInlineSuggestionData(entry[0])?.createdAt ?? Date.now()
|
|
157
|
+
};
|
|
158
|
+
return defaultProps;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
//#endregion
|
|
162
|
+
//#region src/lib/transforms/addMarkSuggestion.ts
|
|
163
|
+
const getAddMarkProps = () => {
|
|
164
|
+
return {
|
|
165
|
+
id: nanoid(),
|
|
166
|
+
createdAt: Date.now()
|
|
167
|
+
};
|
|
168
|
+
};
|
|
169
|
+
const addMarkSuggestion = (editor, key, value) => {
|
|
170
|
+
editor.getApi(BaseSuggestionPlugin).suggestion.withoutSuggestions(() => {
|
|
171
|
+
const { id, createdAt } = getAddMarkProps();
|
|
172
|
+
const match = (n) => {
|
|
173
|
+
if (!TextApi.isText(n)) return false;
|
|
174
|
+
if (n[KEYS.suggestion]) {
|
|
175
|
+
if (getInlineSuggestionData(n)?.type === "update") return true;
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
return true;
|
|
179
|
+
};
|
|
180
|
+
editor.tf.setNodes({
|
|
181
|
+
[key]: value,
|
|
182
|
+
[getSuggestionKey(id)]: {
|
|
183
|
+
id,
|
|
184
|
+
createdAt,
|
|
185
|
+
newProperties: { [key]: value },
|
|
186
|
+
type: "update",
|
|
187
|
+
userId: editor.getOptions(BaseSuggestionPlugin).currentUserId
|
|
188
|
+
},
|
|
189
|
+
[KEYS.suggestion]: true
|
|
190
|
+
}, {
|
|
191
|
+
match,
|
|
192
|
+
split: true
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
//#endregion
|
|
198
|
+
//#region src/lib/transforms/setSuggestionNodes.ts
|
|
199
|
+
const setSuggestionNodes = (editor, options) => {
|
|
200
|
+
const at = getAt(editor, options?.at) ?? editor.selection;
|
|
201
|
+
if (!at) return;
|
|
202
|
+
const { suggestionId = nanoid() } = options ?? {};
|
|
203
|
+
const nodeEntries = [...editor.api.nodes({
|
|
204
|
+
match: (n) => ElementApi.isElement(n) && editor.api.isInline(n),
|
|
205
|
+
...options
|
|
206
|
+
})];
|
|
207
|
+
editor.tf.withoutNormalizing(() => {
|
|
208
|
+
const data = {
|
|
209
|
+
id: suggestionId,
|
|
210
|
+
createdAt: options?.createdAt ?? Date.now(),
|
|
211
|
+
type: "remove",
|
|
212
|
+
userId: editor.getOptions(BaseSuggestionPlugin).currentUserId
|
|
213
|
+
};
|
|
214
|
+
const props = {
|
|
215
|
+
[getSuggestionKey(suggestionId)]: data,
|
|
216
|
+
[KEYS.suggestion]: true
|
|
217
|
+
};
|
|
218
|
+
editor.tf.setNodes(props, {
|
|
219
|
+
at,
|
|
220
|
+
marks: true
|
|
221
|
+
});
|
|
222
|
+
nodeEntries.forEach(([, path]) => {
|
|
223
|
+
editor.tf.setNodes(props, {
|
|
224
|
+
at: path,
|
|
225
|
+
match: (n) => ElementApi.isElement(n) && editor.api.isInline(n),
|
|
226
|
+
...options
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
//#endregion
|
|
233
|
+
//#region src/lib/transforms/deleteSuggestion.ts
|
|
234
|
+
/**
|
|
235
|
+
* Suggest deletion one character at a time until target point is reached.
|
|
236
|
+
* Suggest additions are safely deleted.
|
|
237
|
+
*/
|
|
238
|
+
const deleteSuggestion = (editor, at, { reverse } = {}) => {
|
|
239
|
+
let resId;
|
|
240
|
+
editor.tf.withoutNormalizing(() => {
|
|
241
|
+
const { anchor: from, focus: to } = at;
|
|
242
|
+
const { id, createdAt } = findSuggestionProps(editor, {
|
|
243
|
+
at: from,
|
|
244
|
+
type: "remove"
|
|
245
|
+
});
|
|
246
|
+
resId = id;
|
|
247
|
+
const toRef = editor.api.pointRef(to);
|
|
248
|
+
let pointCurrent;
|
|
249
|
+
while (true) {
|
|
250
|
+
pointCurrent = editor.selection?.anchor;
|
|
251
|
+
if (!pointCurrent) break;
|
|
252
|
+
const pointTarget = toRef.current;
|
|
253
|
+
if (!pointTarget) break;
|
|
254
|
+
if (!editor.api.isAt({
|
|
255
|
+
at: {
|
|
256
|
+
anchor: pointCurrent,
|
|
257
|
+
focus: pointTarget
|
|
258
|
+
},
|
|
259
|
+
blocks: true
|
|
260
|
+
})) {
|
|
261
|
+
if (editor.api.string(reverse ? {
|
|
262
|
+
anchor: pointTarget,
|
|
263
|
+
focus: pointCurrent
|
|
264
|
+
} : {
|
|
265
|
+
anchor: pointCurrent,
|
|
266
|
+
focus: pointTarget
|
|
267
|
+
}).length === 0) break;
|
|
268
|
+
}
|
|
269
|
+
const pointNext = (reverse ? editor.api.before : editor.api.after)(pointCurrent, { unit: "character" });
|
|
270
|
+
if (!pointNext) break;
|
|
271
|
+
let range = reverse ? {
|
|
272
|
+
anchor: pointNext,
|
|
273
|
+
focus: pointCurrent
|
|
274
|
+
} : {
|
|
275
|
+
anchor: pointCurrent,
|
|
276
|
+
focus: pointNext
|
|
277
|
+
};
|
|
278
|
+
range = editor.api.unhangRange(range, { character: true });
|
|
279
|
+
const entryBlock = editor.api.node({
|
|
280
|
+
at: pointCurrent,
|
|
281
|
+
block: true,
|
|
282
|
+
match: (n) => n[KEYS.suggestion] && TextApi.isText(n) && getInlineSuggestionData(n)?.type === "insert" && isCurrentUserSuggestion(editor, n)
|
|
283
|
+
});
|
|
284
|
+
if (entryBlock && editor.api.isStart(pointCurrent, entryBlock[1]) && editor.api.isEmpty(entryBlock[0])) {
|
|
285
|
+
editor.tf.removeNodes({ at: entryBlock[1] });
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
if (editor.api.isAt({
|
|
289
|
+
at: range,
|
|
290
|
+
blocks: true
|
|
291
|
+
})) {
|
|
292
|
+
const previousAboveNode = editor.api.above({ at: range.anchor });
|
|
293
|
+
if (previousAboveNode && ElementApi.isElement(previousAboveNode[0])) {
|
|
294
|
+
const isBlockSuggestion = editor.getApi(BaseSuggestionPlugin).suggestion.isBlockSuggestion(previousAboveNode[0]);
|
|
295
|
+
if (isBlockSuggestion) {
|
|
296
|
+
const node = previousAboveNode[0];
|
|
297
|
+
if (node.suggestion.type === "insert") editor.getApi(BaseSuggestionPlugin).suggestion.withoutSuggestions(() => {
|
|
298
|
+
editor.tf.unsetNodes([KEYS.suggestion], { at: previousAboveNode[1] });
|
|
299
|
+
editor.tf.mergeNodes({ at: PathApi.next(previousAboveNode[1]) });
|
|
300
|
+
});
|
|
301
|
+
if (node.suggestion.type === "remove") editor.tf.move({
|
|
302
|
+
reverse,
|
|
303
|
+
unit: "character"
|
|
304
|
+
});
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
if (!isBlockSuggestion) {
|
|
308
|
+
editor.tf.setNodes({ [KEYS.suggestion]: {
|
|
309
|
+
id,
|
|
310
|
+
createdAt,
|
|
311
|
+
type: "remove",
|
|
312
|
+
userId: editor.getOptions(BaseSuggestionPlugin).currentUserId
|
|
313
|
+
} }, { at: previousAboveNode[1] });
|
|
314
|
+
editor.tf.move({
|
|
315
|
+
reverse,
|
|
316
|
+
unit: "character"
|
|
317
|
+
});
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
if (PointApi.equals(pointCurrent, editor.selection.anchor)) editor.tf.move({
|
|
324
|
+
reverse,
|
|
325
|
+
unit: "character"
|
|
326
|
+
});
|
|
327
|
+
if (editor.getApi(BaseSuggestionPlugin).suggestion.node({
|
|
328
|
+
at: range,
|
|
329
|
+
isText: true,
|
|
330
|
+
match: (n) => TextApi.isText(n) && getInlineSuggestionData(n)?.type === "insert" && isCurrentUserSuggestion(editor, n)
|
|
331
|
+
})) {
|
|
332
|
+
editor.tf.delete({
|
|
333
|
+
at: range,
|
|
334
|
+
unit: "character"
|
|
335
|
+
});
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
setSuggestionNodes(editor, {
|
|
339
|
+
at: range,
|
|
340
|
+
createdAt,
|
|
341
|
+
suggestionDeletion: true,
|
|
342
|
+
suggestionId: id
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
return resId;
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
//#endregion
|
|
350
|
+
//#region src/lib/transforms/deleteFragmentSuggestion.ts
|
|
351
|
+
const deleteFragmentSuggestion = (editor, { reverse } = {}) => {
|
|
352
|
+
let resId;
|
|
353
|
+
editor.tf.withoutNormalizing(() => {
|
|
354
|
+
const selection = editor.selection;
|
|
355
|
+
const [start, end] = editor.api.edges(selection);
|
|
356
|
+
if (reverse) {
|
|
357
|
+
editor.tf.collapse({ edge: "end" });
|
|
358
|
+
resId = deleteSuggestion(editor, {
|
|
359
|
+
anchor: end,
|
|
360
|
+
focus: start
|
|
361
|
+
}, { reverse: true });
|
|
362
|
+
} else {
|
|
363
|
+
editor.tf.collapse({ edge: "start" });
|
|
364
|
+
resId = deleteSuggestion(editor, {
|
|
365
|
+
anchor: start,
|
|
366
|
+
focus: end
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
return resId;
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
//#endregion
|
|
374
|
+
//#region src/lib/transforms/insertFragmentSuggestion.ts
|
|
375
|
+
const insertFragmentSuggestion = (editor, fragment, { insertFragment = editor.tf.insertFragment } = {}) => {
|
|
376
|
+
editor.tf.withoutNormalizing(() => {
|
|
377
|
+
deleteFragmentSuggestion(editor);
|
|
378
|
+
const { id, createdAt } = findSuggestionProps(editor, {
|
|
379
|
+
at: editor.selection,
|
|
380
|
+
type: "insert"
|
|
381
|
+
});
|
|
382
|
+
fragment.forEach((n) => {
|
|
383
|
+
if (TextApi.isText(n)) {
|
|
384
|
+
if (!n[KEYS.suggestion]) n[KEYS.suggestion] = true;
|
|
385
|
+
getSuggestionKeys(n).forEach((key) => {
|
|
386
|
+
delete n[key];
|
|
387
|
+
});
|
|
388
|
+
n[getSuggestionKey(id)] = {
|
|
389
|
+
id,
|
|
390
|
+
createdAt,
|
|
391
|
+
type: "insert",
|
|
392
|
+
userId: editor.getOptions(BaseSuggestionPlugin).currentUserId
|
|
393
|
+
};
|
|
394
|
+
} else n[KEYS.suggestion] = {
|
|
395
|
+
id,
|
|
396
|
+
createdAt,
|
|
397
|
+
type: "insert",
|
|
398
|
+
userId: editor.getOptions(BaseSuggestionPlugin).currentUserId
|
|
399
|
+
};
|
|
400
|
+
});
|
|
401
|
+
editor.getApi(BaseSuggestionPlugin).suggestion.withoutSuggestions(() => {
|
|
402
|
+
insertFragment(fragment);
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
//#endregion
|
|
408
|
+
//#region src/lib/transforms/insertTextSuggestion.ts
|
|
409
|
+
const insertTextSuggestion = (editor, text) => {
|
|
410
|
+
editor.tf.withoutNormalizing(() => {
|
|
411
|
+
let resId;
|
|
412
|
+
const { id, createdAt } = findSuggestionProps(editor, {
|
|
413
|
+
at: editor.selection,
|
|
414
|
+
type: "insert"
|
|
415
|
+
});
|
|
416
|
+
if (editor.api.isExpanded()) resId = deleteFragmentSuggestion(editor);
|
|
417
|
+
editor.getApi(BaseSuggestionPlugin).suggestion.withoutSuggestions(() => {
|
|
418
|
+
editor.tf.insertNodes({
|
|
419
|
+
[getSuggestionKey(resId ?? id)]: {
|
|
420
|
+
id: resId ?? id,
|
|
421
|
+
createdAt,
|
|
422
|
+
type: "insert",
|
|
423
|
+
userId: editor.getOptions(BaseSuggestionPlugin).currentUserId
|
|
424
|
+
},
|
|
425
|
+
suggestion: true,
|
|
426
|
+
text
|
|
427
|
+
}, {
|
|
428
|
+
at: editor.selection,
|
|
429
|
+
select: true
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
//#endregion
|
|
436
|
+
//#region src/lib/transforms/removeMarkSuggestion.ts
|
|
437
|
+
const getRemoveMarkProps = () => {
|
|
438
|
+
return {
|
|
439
|
+
id: nanoid(),
|
|
440
|
+
createdAt: Date.now()
|
|
441
|
+
};
|
|
442
|
+
};
|
|
443
|
+
const removeMarkSuggestion = (editor, key) => {
|
|
444
|
+
editor.getApi(BaseSuggestionPlugin).suggestion.withoutSuggestions(() => {
|
|
445
|
+
const { id, createdAt } = getRemoveMarkProps();
|
|
446
|
+
const match = (n) => {
|
|
447
|
+
if (!TextApi.isText(n)) return false;
|
|
448
|
+
if (n[KEYS.suggestion]) {
|
|
449
|
+
if (getInlineSuggestionData(n)?.type === "update") return true;
|
|
450
|
+
return false;
|
|
451
|
+
}
|
|
452
|
+
return true;
|
|
453
|
+
};
|
|
454
|
+
editor.tf.unsetNodes(key, { match });
|
|
455
|
+
editor.tf.setNodes({
|
|
456
|
+
[getSuggestionKey(id)]: {
|
|
457
|
+
id,
|
|
458
|
+
createdAt,
|
|
459
|
+
properties: { [key]: void 0 },
|
|
460
|
+
type: "update",
|
|
461
|
+
userId: editor.getOptions(BaseSuggestionPlugin).currentUserId
|
|
462
|
+
},
|
|
463
|
+
[KEYS.suggestion]: true
|
|
464
|
+
}, { match });
|
|
465
|
+
});
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
//#endregion
|
|
469
|
+
//#region src/lib/transforms/removeNodesSuggestion.ts
|
|
470
|
+
const removeNodesSuggestion = (editor, nodes) => {
|
|
471
|
+
if (nodes.length === 0) return;
|
|
472
|
+
const { id, createdAt } = findSuggestionProps(editor, {
|
|
473
|
+
at: editor.selection,
|
|
474
|
+
type: "remove"
|
|
475
|
+
});
|
|
476
|
+
nodes.forEach(([, blockPath]) => {
|
|
477
|
+
editor.tf.setNodes({ [KEYS.suggestion]: {
|
|
478
|
+
id,
|
|
479
|
+
createdAt,
|
|
480
|
+
type: "remove"
|
|
481
|
+
} }, { at: blockPath });
|
|
482
|
+
});
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
//#endregion
|
|
486
|
+
//#region src/lib/withSuggestion.ts
|
|
487
|
+
const withSuggestion = ({ api, editor, getOptions, tf: { addMark, apply, deleteBackward, deleteForward, deleteFragment, insertBreak, insertFragment, insertNodes, insertText, normalizeNode, removeMark, removeNodes } }) => ({ transforms: {
|
|
488
|
+
addMark(key, value) {
|
|
489
|
+
if (getOptions().isSuggesting && api.isExpanded()) return addMarkSuggestion(editor, key, value);
|
|
490
|
+
return addMark(key, value);
|
|
491
|
+
},
|
|
492
|
+
apply(operation) {
|
|
493
|
+
return apply(operation);
|
|
494
|
+
},
|
|
495
|
+
deleteBackward(unit) {
|
|
496
|
+
const selection = editor.selection;
|
|
497
|
+
const pointTarget = editor.api.before(selection, { unit });
|
|
498
|
+
if (getOptions().isSuggesting) {
|
|
499
|
+
const node = editor.api.above();
|
|
500
|
+
if (node?.[0][KEYS.suggestion] && !node?.[0].suggestion.isLineBreak) return deleteBackward(unit);
|
|
501
|
+
if (!pointTarget) return;
|
|
502
|
+
deleteSuggestion(editor, {
|
|
503
|
+
anchor: selection.anchor,
|
|
504
|
+
focus: pointTarget
|
|
505
|
+
}, { reverse: true });
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
if (pointTarget) {
|
|
509
|
+
if (editor.api.isAt({
|
|
510
|
+
at: {
|
|
511
|
+
anchor: selection.anchor,
|
|
512
|
+
focus: pointTarget
|
|
513
|
+
},
|
|
514
|
+
blocks: true
|
|
515
|
+
})) editor.tf.unsetNodes([KEYS.suggestion], { at: pointTarget });
|
|
516
|
+
}
|
|
517
|
+
deleteBackward(unit);
|
|
518
|
+
},
|
|
519
|
+
deleteForward(unit) {
|
|
520
|
+
if (getOptions().isSuggesting) {
|
|
521
|
+
const selection = editor.selection;
|
|
522
|
+
const pointTarget = editor.api.after(selection, { unit });
|
|
523
|
+
if (!pointTarget) return;
|
|
524
|
+
deleteSuggestion(editor, {
|
|
525
|
+
anchor: selection.anchor,
|
|
526
|
+
focus: pointTarget
|
|
527
|
+
});
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
deleteForward(unit);
|
|
531
|
+
},
|
|
532
|
+
deleteFragment(direction) {
|
|
533
|
+
if (getOptions().isSuggesting) {
|
|
534
|
+
deleteFragmentSuggestion(editor, { reverse: true });
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
deleteFragment(direction);
|
|
538
|
+
},
|
|
539
|
+
insertBreak() {
|
|
540
|
+
if (getOptions().isSuggesting) {
|
|
541
|
+
const [node, path] = editor.api.above();
|
|
542
|
+
if (path.length > 1 || node.type !== editor.getType(KEYS.p)) return insertTextSuggestion(editor, "\n");
|
|
543
|
+
const { id, createdAt } = findSuggestionProps(editor, {
|
|
544
|
+
at: editor.selection,
|
|
545
|
+
type: "insert"
|
|
546
|
+
});
|
|
547
|
+
insertBreak();
|
|
548
|
+
editor.tf.withoutMerging(() => {
|
|
549
|
+
editor.tf.setNodes({ [KEYS.suggestion]: {
|
|
550
|
+
id,
|
|
551
|
+
createdAt,
|
|
552
|
+
isLineBreak: true,
|
|
553
|
+
type: "insert",
|
|
554
|
+
userId: editor.getOptions(BaseSuggestionPlugin).currentUserId
|
|
555
|
+
} }, { at: path });
|
|
556
|
+
});
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
insertBreak();
|
|
560
|
+
},
|
|
561
|
+
insertFragment(fragment) {
|
|
562
|
+
if (getOptions().isSuggesting) {
|
|
563
|
+
insertFragmentSuggestion(editor, fragment, { insertFragment });
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
insertFragment(fragment);
|
|
567
|
+
},
|
|
568
|
+
insertNodes(nodes, options) {
|
|
569
|
+
if (getOptions().isSuggesting) {
|
|
570
|
+
const nodesArray = Array.isArray(nodes) ? nodes : [nodes];
|
|
571
|
+
if (nodesArray.some((n) => n.type === "slash_input")) {
|
|
572
|
+
api.suggestion.withoutSuggestions(() => {
|
|
573
|
+
insertNodes(nodes, options);
|
|
574
|
+
});
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
return insertNodes(nodesArray.map((node) => ({
|
|
578
|
+
...node,
|
|
579
|
+
[KEYS.suggestion]: {
|
|
580
|
+
id: nanoid(),
|
|
581
|
+
createdAt: Date.now(),
|
|
582
|
+
type: "insert",
|
|
583
|
+
userId: editor.getOptions(BaseSuggestionPlugin).currentUserId
|
|
584
|
+
}
|
|
585
|
+
})), options);
|
|
586
|
+
}
|
|
587
|
+
return insertNodes(nodes, options);
|
|
588
|
+
},
|
|
589
|
+
insertText(text, options) {
|
|
590
|
+
if (getOptions().isSuggesting) {
|
|
591
|
+
const node = editor.api.above();
|
|
592
|
+
if (node?.[0][KEYS.suggestion] && !node?.[0].suggestion.isLineBreak) return insertText(text, options);
|
|
593
|
+
insertTextSuggestion(editor, text);
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
insertText(text, options);
|
|
597
|
+
},
|
|
598
|
+
normalizeNode(entry) {
|
|
599
|
+
api.suggestion.withoutSuggestions(() => {
|
|
600
|
+
const [node, path] = entry;
|
|
601
|
+
const inlineSuggestion = ElementApi.isElement(node) && editor.api.isInline(node) || TextApi.isText(node);
|
|
602
|
+
if (node[KEYS.suggestion] && inlineSuggestion && !getSuggestionKeyId(node)) {
|
|
603
|
+
editor.tf.unsetNodes([KEYS.suggestion, "suggestionData"], { at: path });
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
if (node[KEYS.suggestion] && inlineSuggestion && !getInlineSuggestionData(node)?.userId) {
|
|
607
|
+
if (getInlineSuggestionData(node)?.type === "remove") editor.tf.unsetNodes([KEYS.suggestion, getSuggestionKeyId(node)], { at: path });
|
|
608
|
+
else editor.tf.removeNodes({ at: path });
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
normalizeNode(entry);
|
|
612
|
+
});
|
|
613
|
+
},
|
|
614
|
+
removeMark(key) {
|
|
615
|
+
if (getOptions().isSuggesting && api.isExpanded()) return removeMarkSuggestion(editor, key);
|
|
616
|
+
return removeMark(key);
|
|
617
|
+
},
|
|
618
|
+
removeNodes(options) {
|
|
619
|
+
if (getOptions().isSuggesting) {
|
|
620
|
+
const nodes = [...editor.api.nodes(options)];
|
|
621
|
+
if (nodes.some(([n]) => n.type === "slash_input")) {
|
|
622
|
+
api.suggestion.withoutSuggestions(() => {
|
|
623
|
+
removeNodes(options);
|
|
624
|
+
});
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
return removeNodesSuggestion(editor, nodes);
|
|
628
|
+
}
|
|
629
|
+
return removeNodes(options);
|
|
630
|
+
}
|
|
631
|
+
} });
|
|
632
|
+
|
|
633
|
+
//#endregion
|
|
634
|
+
//#region src/lib/BaseSuggestionPlugin.ts
|
|
635
|
+
const BaseSuggestionPlugin = createTSlatePlugin({
|
|
636
|
+
key: KEYS.suggestion,
|
|
637
|
+
node: { isLeaf: true },
|
|
638
|
+
options: {
|
|
639
|
+
currentUserId: "alice",
|
|
640
|
+
isSuggesting: false
|
|
641
|
+
},
|
|
642
|
+
rules: { selection: { affinity: "outward" } }
|
|
643
|
+
}).overrideEditor(withSuggestion).extendApi(({ api, editor, getOption, setOption, type }) => ({
|
|
644
|
+
dataList: (node) => Object.keys(node).filter((key) => key.startsWith(`${KEYS.suggestion}_`)).map((key) => node[key]),
|
|
645
|
+
isBlockSuggestion: (node) => ElementApi.isElement(node) && !editor.api.isInline(node) && "suggestion" in node,
|
|
646
|
+
node: (options = {}) => {
|
|
647
|
+
const { id, isText, ...rest } = options;
|
|
648
|
+
return editor.api.node({
|
|
649
|
+
match: (n) => {
|
|
650
|
+
if (!n[type]) return false;
|
|
651
|
+
if (isText && !TextApi.isText(n)) return false;
|
|
652
|
+
if (id) {
|
|
653
|
+
if (TextApi.isText(n)) return !!n[getSuggestionKey(id)];
|
|
654
|
+
if (ElementApi.isElement(n) && api.suggestion.isBlockSuggestion(n)) return n.suggestion.id === id;
|
|
655
|
+
}
|
|
656
|
+
return true;
|
|
657
|
+
},
|
|
658
|
+
...rest
|
|
659
|
+
});
|
|
660
|
+
},
|
|
661
|
+
nodeId: (node) => {
|
|
662
|
+
if (TextApi.isText(node) || ElementApi.isElement(node) && editor.api.isInline(node)) {
|
|
663
|
+
const keyId = getSuggestionKeyId(node);
|
|
664
|
+
if (!keyId) return;
|
|
665
|
+
return keyId.replace(`${type}_`, "");
|
|
666
|
+
}
|
|
667
|
+
if (api.suggestion.isBlockSuggestion(node)) return node.suggestion.id;
|
|
668
|
+
},
|
|
669
|
+
nodes: (options = {}) => {
|
|
670
|
+
const { transient } = options;
|
|
671
|
+
const at = getAt(editor, options.at) ?? [];
|
|
672
|
+
return [...editor.api.nodes({
|
|
673
|
+
...options,
|
|
674
|
+
at,
|
|
675
|
+
mode: "all",
|
|
676
|
+
match: (n) => n[type] && (transient ? n[getTransientSuggestionKey()] : true)
|
|
677
|
+
})];
|
|
678
|
+
},
|
|
679
|
+
suggestionData: (node) => {
|
|
680
|
+
if (TextApi.isText(node) || ElementApi.isElement(node) && editor.api.isInline(node)) {
|
|
681
|
+
const keyId = getSuggestionKeyId(node);
|
|
682
|
+
if (!keyId) return;
|
|
683
|
+
return node[keyId];
|
|
684
|
+
}
|
|
685
|
+
if (api.suggestion.isBlockSuggestion(node)) return node.suggestion;
|
|
686
|
+
},
|
|
687
|
+
withoutSuggestions: (fn) => {
|
|
688
|
+
const prev = getOption("isSuggesting");
|
|
689
|
+
setOption("isSuggesting", false);
|
|
690
|
+
fn();
|
|
691
|
+
setOption("isSuggesting", prev);
|
|
692
|
+
}
|
|
693
|
+
}));
|
|
694
|
+
|
|
695
|
+
//#endregion
|
|
696
|
+
//#region src/lib/transforms/acceptSuggestion.ts
|
|
697
|
+
const acceptSuggestion = (editor, description) => {
|
|
698
|
+
editor.tf.withoutNormalizing(() => {
|
|
699
|
+
[...editor.api.nodes({
|
|
700
|
+
at: [],
|
|
701
|
+
match: (n) => {
|
|
702
|
+
if (!ElementApi.isElement(n)) return false;
|
|
703
|
+
if (editor.getApi(BaseSuggestionPlugin).suggestion.isBlockSuggestion(n)) {
|
|
704
|
+
const suggestionElement = n;
|
|
705
|
+
return suggestionElement.suggestion.type === "remove" && suggestionElement.suggestion.isLineBreak && suggestionElement.suggestion.id === description.suggestionId;
|
|
706
|
+
}
|
|
707
|
+
return false;
|
|
708
|
+
}
|
|
709
|
+
})].reverse().forEach(([, path]) => {
|
|
710
|
+
editor.tf.mergeNodes({ at: PathApi.next(path) });
|
|
711
|
+
});
|
|
712
|
+
editor.tf.unsetNodes([
|
|
713
|
+
description.keyId,
|
|
714
|
+
KEYS.suggestion,
|
|
715
|
+
getTransientSuggestionKey()
|
|
716
|
+
], {
|
|
717
|
+
at: [],
|
|
718
|
+
mode: "all",
|
|
719
|
+
match: (n) => {
|
|
720
|
+
if (TextApi.isText(n) || ElementApi.isElement(n) && editor.api.isInline(n)) {
|
|
721
|
+
const suggestionDataList = editor.getApi(BaseSuggestionPlugin).suggestion.dataList(n);
|
|
722
|
+
if (suggestionDataList.some((data) => data.type === "update")) return suggestionDataList.some((d) => d.id === description.suggestionId);
|
|
723
|
+
const suggestionData = getInlineSuggestionData(n);
|
|
724
|
+
if (suggestionData) return suggestionData.type === "insert" && suggestionData.id === description.suggestionId;
|
|
725
|
+
return false;
|
|
726
|
+
}
|
|
727
|
+
if (ElementApi.isElement(n) && editor.getApi(BaseSuggestionPlugin).suggestion.isBlockSuggestion(n)) {
|
|
728
|
+
const suggestionData = n.suggestion;
|
|
729
|
+
if (suggestionData) {
|
|
730
|
+
if (suggestionData.isLineBreak) return suggestionData.id === description.suggestionId;
|
|
731
|
+
return suggestionData.type === "insert" && suggestionData.id === description.suggestionId;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
return false;
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
editor.tf.removeNodes({
|
|
738
|
+
at: [],
|
|
739
|
+
mode: "all",
|
|
740
|
+
match: (n) => {
|
|
741
|
+
if (TextApi.isText(n) || ElementApi.isElement(n) && editor.api.isInline(n)) {
|
|
742
|
+
const suggestionData = getInlineSuggestionData(n);
|
|
743
|
+
if (suggestionData) return suggestionData.type === "remove" && suggestionData.id === description.suggestionId;
|
|
744
|
+
return false;
|
|
745
|
+
}
|
|
746
|
+
if (ElementApi.isElement(n) && editor.getApi(BaseSuggestionPlugin).suggestion.isBlockSuggestion(n)) {
|
|
747
|
+
const suggestionData = n.suggestion;
|
|
748
|
+
if (suggestionData) {
|
|
749
|
+
const isLineBreak = suggestionData.isLineBreak;
|
|
750
|
+
return suggestionData.type === "remove" && suggestionData.id === description.suggestionId && !isLineBreak;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
return false;
|
|
754
|
+
}
|
|
755
|
+
});
|
|
756
|
+
});
|
|
757
|
+
};
|
|
758
|
+
|
|
759
|
+
//#endregion
|
|
760
|
+
//#region src/lib/transforms/getSuggestionProps.ts
|
|
761
|
+
const getSuggestionProps = (editor, node, { id = nanoid(), createdAt = Date.now(), suggestionDeletion, suggestionUpdate, transient } = {}) => {
|
|
762
|
+
const type = suggestionDeletion ? "remove" : suggestionUpdate ? "update" : "insert";
|
|
763
|
+
const isElement = ElementApi.isElement(node);
|
|
764
|
+
const suggestionData = {
|
|
765
|
+
id,
|
|
766
|
+
createdAt,
|
|
767
|
+
type,
|
|
768
|
+
userId: editor.getOptions(BaseSuggestionPlugin).currentUserId
|
|
769
|
+
};
|
|
770
|
+
if (isElement) return { [KEYS.suggestion]: suggestionData };
|
|
771
|
+
const res = {
|
|
772
|
+
[getSuggestionKey(id)]: suggestionData,
|
|
773
|
+
[KEYS.suggestion]: true
|
|
774
|
+
};
|
|
775
|
+
if (transient) res[getTransientSuggestionKey()] = true;
|
|
776
|
+
return res;
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
//#endregion
|
|
780
|
+
//#region src/lib/transforms/rejectSuggestion.ts
|
|
781
|
+
const rejectSuggestion = (editor, description) => {
|
|
782
|
+
editor.tf.withoutNormalizing(() => {
|
|
783
|
+
[...editor.api.nodes({
|
|
784
|
+
at: [],
|
|
785
|
+
match: (n) => {
|
|
786
|
+
if (!ElementApi.isElement(n)) return false;
|
|
787
|
+
if (editor.getApi(BaseSuggestionPlugin).suggestion.isBlockSuggestion(n)) {
|
|
788
|
+
const suggestionElement = n;
|
|
789
|
+
return suggestionElement.suggestion.type === "insert" && suggestionElement.suggestion.isLineBreak && suggestionElement.suggestion.id === description.suggestionId;
|
|
790
|
+
}
|
|
791
|
+
return false;
|
|
792
|
+
}
|
|
793
|
+
})].reverse().forEach(([, path]) => {
|
|
794
|
+
editor.tf.mergeNodes({ at: PathApi.next(path) });
|
|
795
|
+
});
|
|
796
|
+
editor.tf.unsetNodes([
|
|
797
|
+
description.keyId,
|
|
798
|
+
KEYS.suggestion,
|
|
799
|
+
getTransientSuggestionKey()
|
|
800
|
+
], {
|
|
801
|
+
at: [],
|
|
802
|
+
mode: "all",
|
|
803
|
+
match: (n) => {
|
|
804
|
+
if (TextApi.isText(n)) {
|
|
805
|
+
const suggestionData = getInlineSuggestionData(n);
|
|
806
|
+
if (suggestionData) return suggestionData.type === "remove" && suggestionData.id === description.suggestionId;
|
|
807
|
+
return false;
|
|
808
|
+
}
|
|
809
|
+
if (ElementApi.isElement(n) && editor.getApi(BaseSuggestionPlugin).suggestion.isBlockSuggestion(n)) {
|
|
810
|
+
const suggestionElement = n;
|
|
811
|
+
if (suggestionElement.suggestion.isLineBreak) return suggestionElement.suggestion.id === description.suggestionId;
|
|
812
|
+
return suggestionElement.suggestion.type === "remove" && suggestionElement.suggestion.id === description.suggestionId;
|
|
813
|
+
}
|
|
814
|
+
return false;
|
|
815
|
+
}
|
|
816
|
+
});
|
|
817
|
+
editor.tf.removeNodes({
|
|
818
|
+
at: [],
|
|
819
|
+
mode: "all",
|
|
820
|
+
match: (n) => {
|
|
821
|
+
if (TextApi.isText(n)) {
|
|
822
|
+
const suggestionData = getInlineSuggestionData(n);
|
|
823
|
+
if (suggestionData) return suggestionData.type === "insert" && suggestionData.id === description.suggestionId;
|
|
824
|
+
return false;
|
|
825
|
+
}
|
|
826
|
+
if (ElementApi.isElement(n) && editor.getApi(BaseSuggestionPlugin).suggestion.isBlockSuggestion(n)) {
|
|
827
|
+
const suggestionElement = n;
|
|
828
|
+
return suggestionElement.suggestion.type === "insert" && suggestionElement.suggestion.id === description.suggestionId && !suggestionElement.suggestion.isLineBreak;
|
|
829
|
+
}
|
|
830
|
+
return false;
|
|
831
|
+
}
|
|
832
|
+
});
|
|
833
|
+
[...editor.api.nodes({
|
|
834
|
+
at: [],
|
|
835
|
+
match: (n) => {
|
|
836
|
+
if (ElementApi.isElement(n)) return false;
|
|
837
|
+
if (TextApi.isText(n)) {
|
|
838
|
+
const datalist = editor.getApi(BaseSuggestionPlugin).suggestion.dataList(n);
|
|
839
|
+
if (datalist.length > 0) return datalist.some((data) => data.type === "update" && data.id === description.suggestionId);
|
|
840
|
+
return false;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
})].forEach(([node, path]) => {
|
|
844
|
+
const targetData = editor.getApi(BaseSuggestionPlugin).suggestion.dataList(node).find((data) => data.type === "update" && data.id === description.suggestionId);
|
|
845
|
+
if (!targetData) return;
|
|
846
|
+
if ("newProperties" in targetData) {
|
|
847
|
+
const unsetProps = Object.keys(targetData.newProperties).filter((key) => targetData.newProperties[key]);
|
|
848
|
+
editor.tf.unsetNodes([...unsetProps], { at: path });
|
|
849
|
+
}
|
|
850
|
+
if ("properties" in targetData) {
|
|
851
|
+
const addProps = Object.keys(targetData.properties).filter((key) => !targetData.properties[key]);
|
|
852
|
+
editor.tf.setNodes(Object.fromEntries(addProps.map((key) => [key, true])), { at: path });
|
|
853
|
+
}
|
|
854
|
+
editor.tf.unsetNodes([getSuggestionKey(targetData.id)], { at: path });
|
|
855
|
+
});
|
|
856
|
+
});
|
|
857
|
+
};
|
|
858
|
+
|
|
859
|
+
//#endregion
|
|
860
|
+
//#region src/lib/diffToSuggestions.ts
|
|
861
|
+
function diffToSuggestions(editor, doc0, doc1, { getDeleteProps = (node) => getSuggestionProps(editor, node, { suggestionDeletion: true }), getInsertProps = (node) => getSuggestionProps(editor, node), getUpdateProps = (node, _properties, newProperties) => getSuggestionProps(editor, node, { suggestionUpdate: newProperties }), isInline = editor.api.isInline, ...options } = {}) {
|
|
862
|
+
const values = computeDiff(doc0, doc1, {
|
|
863
|
+
getDeleteProps,
|
|
864
|
+
getInsertProps,
|
|
865
|
+
getUpdateProps,
|
|
866
|
+
isInline,
|
|
867
|
+
...options
|
|
868
|
+
});
|
|
869
|
+
const traverseNodes = (nodes) => {
|
|
870
|
+
return nodes.map((node, index) => {
|
|
871
|
+
if (ElementApi.isElement(node) && "children" in node) return {
|
|
872
|
+
...node,
|
|
873
|
+
children: traverseNodes(node.children)
|
|
874
|
+
};
|
|
875
|
+
if (TextApi.isText(node) && node[KEYS.suggestion]) return unifyAdjacentSuggestionIds(node, index, nodes, editor);
|
|
876
|
+
return node;
|
|
877
|
+
});
|
|
878
|
+
};
|
|
879
|
+
return traverseNodes(values);
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* Unifies the ID of adjacent insert and remove suggestions. When an insert
|
|
883
|
+
* suggestion follows a remove suggestion, the insert suggestion inherits the ID
|
|
884
|
+
* and creation time from the remove suggestion. This allows the UI to treat
|
|
885
|
+
* them as a single suggestion for display and interaction purposes.
|
|
886
|
+
*/
|
|
887
|
+
function unifyAdjacentSuggestionIds(node, index, nodes, editor) {
|
|
888
|
+
const api = editor.getApi(BaseSuggestionPlugin);
|
|
889
|
+
const currentNodeData = api.suggestion.suggestionData(node);
|
|
890
|
+
if (currentNodeData?.type === "insert") {
|
|
891
|
+
const previousNode = index > 0 ? nodes[index - 1] : null;
|
|
892
|
+
if (previousNode?.[KEYS.suggestion]) {
|
|
893
|
+
const previousData = api.suggestion.suggestionData(previousNode);
|
|
894
|
+
if (previousData?.type === "remove") {
|
|
895
|
+
const updatedNode = {
|
|
896
|
+
...node,
|
|
897
|
+
[getSuggestionKey(previousData.id)]: {
|
|
898
|
+
...currentNodeData,
|
|
899
|
+
id: previousData.id,
|
|
900
|
+
createdAt: previousData.createdAt
|
|
901
|
+
}
|
|
902
|
+
};
|
|
903
|
+
const key = getSuggestionKey(currentNodeData.id);
|
|
904
|
+
delete updatedNode[key];
|
|
905
|
+
return updatedNode;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
return node;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
//#endregion
|
|
913
|
+
export { SkipSuggestionDeletes as A, getSuggestionUserIdByKey as C, getInlineSuggestionData as D, isSuggestionKey as E, getSuggestionKeyId as O, getSuggestionUserId as S, isCurrentUserSuggestion as T, getTransientSuggestionKey as _, BaseSuggestionPlugin as a, getSuggestionKey as b, removeMarkSuggestion as c, deleteFragmentSuggestion as d, deleteSuggestion as f, findInlineSuggestionNode as g, findSuggestionProps as h, acceptSuggestion as i, keyId2SuggestionId as k, insertTextSuggestion as l, addMarkSuggestion as m, rejectSuggestion as n, withSuggestion as o, setSuggestionNodes as p, getSuggestionProps as r, removeNodesSuggestion as s, diffToSuggestions as t, insertFragmentSuggestion as u, getActiveSuggestionDescriptions as v, getSuggestionUserIds as w, getSuggestionKeys as x, getSuggestionNodeEntries as y };
|
|
914
|
+
//# sourceMappingURL=src-COX5sId2.js.map
|