@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.
@@ -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