@portabletext/editor 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3 -0
  3. package/lib/index.d.mts +911 -0
  4. package/lib/index.d.ts +911 -0
  5. package/lib/index.esm.js +4896 -0
  6. package/lib/index.esm.js.map +1 -0
  7. package/lib/index.js +4874 -0
  8. package/lib/index.js.map +1 -0
  9. package/lib/index.mjs +4896 -0
  10. package/lib/index.mjs.map +1 -0
  11. package/package.json +119 -0
  12. package/src/editor/Editable.tsx +683 -0
  13. package/src/editor/PortableTextEditor.tsx +308 -0
  14. package/src/editor/__tests__/PortableTextEditor.test.tsx +386 -0
  15. package/src/editor/__tests__/PortableTextEditorTester.tsx +116 -0
  16. package/src/editor/__tests__/RangeDecorations.test.tsx +115 -0
  17. package/src/editor/__tests__/handleClick.test.tsx +218 -0
  18. package/src/editor/__tests__/pteWarningsSelfSolving.test.tsx +389 -0
  19. package/src/editor/__tests__/utils.ts +39 -0
  20. package/src/editor/components/DraggableBlock.tsx +287 -0
  21. package/src/editor/components/Element.tsx +279 -0
  22. package/src/editor/components/Leaf.tsx +288 -0
  23. package/src/editor/components/SlateContainer.tsx +81 -0
  24. package/src/editor/components/Synchronizer.tsx +190 -0
  25. package/src/editor/hooks/usePortableTextEditor.ts +23 -0
  26. package/src/editor/hooks/usePortableTextEditorKeyGenerator.ts +24 -0
  27. package/src/editor/hooks/usePortableTextEditorSelection.ts +22 -0
  28. package/src/editor/hooks/usePortableTextEditorValue.ts +16 -0
  29. package/src/editor/hooks/usePortableTextReadOnly.ts +20 -0
  30. package/src/editor/hooks/useSyncValue.test.tsx +125 -0
  31. package/src/editor/hooks/useSyncValue.ts +372 -0
  32. package/src/editor/nodes/DefaultAnnotation.tsx +16 -0
  33. package/src/editor/nodes/DefaultObject.tsx +15 -0
  34. package/src/editor/nodes/index.ts +189 -0
  35. package/src/editor/plugins/__tests__/withEditableAPIDelete.test.tsx +244 -0
  36. package/src/editor/plugins/__tests__/withEditableAPIGetFragment.test.tsx +142 -0
  37. package/src/editor/plugins/__tests__/withEditableAPIInsert.test.tsx +346 -0
  38. package/src/editor/plugins/__tests__/withEditableAPISelectionsOverlapping.test.tsx +162 -0
  39. package/src/editor/plugins/__tests__/withHotkeys.test.tsx +212 -0
  40. package/src/editor/plugins/__tests__/withInsertBreak.test.tsx +204 -0
  41. package/src/editor/plugins/__tests__/withPlaceholderBlock.test.tsx +133 -0
  42. package/src/editor/plugins/__tests__/withPortableTextLists.test.tsx +65 -0
  43. package/src/editor/plugins/__tests__/withPortableTextMarkModel.test.tsx +1377 -0
  44. package/src/editor/plugins/__tests__/withPortableTextSelections.test.tsx +91 -0
  45. package/src/editor/plugins/__tests__/withUndoRedo.test.tsx +115 -0
  46. package/src/editor/plugins/createWithEditableAPI.ts +573 -0
  47. package/src/editor/plugins/createWithHotKeys.ts +304 -0
  48. package/src/editor/plugins/createWithInsertBreak.ts +45 -0
  49. package/src/editor/plugins/createWithInsertData.ts +359 -0
  50. package/src/editor/plugins/createWithMaxBlocks.ts +24 -0
  51. package/src/editor/plugins/createWithObjectKeys.ts +63 -0
  52. package/src/editor/plugins/createWithPatches.ts +274 -0
  53. package/src/editor/plugins/createWithPlaceholderBlock.ts +36 -0
  54. package/src/editor/plugins/createWithPortableTextBlockStyle.ts +91 -0
  55. package/src/editor/plugins/createWithPortableTextLists.ts +160 -0
  56. package/src/editor/plugins/createWithPortableTextMarkModel.ts +441 -0
  57. package/src/editor/plugins/createWithPortableTextSelections.ts +65 -0
  58. package/src/editor/plugins/createWithSchemaTypes.ts +76 -0
  59. package/src/editor/plugins/createWithUndoRedo.ts +494 -0
  60. package/src/editor/plugins/createWithUtils.ts +81 -0
  61. package/src/editor/plugins/index.ts +155 -0
  62. package/src/index.ts +11 -0
  63. package/src/patch/PatchEvent.ts +33 -0
  64. package/src/patch/applyPatch.ts +29 -0
  65. package/src/patch/array.ts +89 -0
  66. package/src/patch/arrayInsert.ts +27 -0
  67. package/src/patch/object.ts +39 -0
  68. package/src/patch/patches.ts +53 -0
  69. package/src/patch/primitive.ts +43 -0
  70. package/src/patch/string.ts +51 -0
  71. package/src/types/editor.ts +576 -0
  72. package/src/types/options.ts +17 -0
  73. package/src/types/patch.ts +65 -0
  74. package/src/types/slate.ts +25 -0
  75. package/src/utils/__tests__/dmpToOperations.test.ts +181 -0
  76. package/src/utils/__tests__/operationToPatches.test.ts +421 -0
  77. package/src/utils/__tests__/patchToOperations.test.ts +293 -0
  78. package/src/utils/__tests__/ranges.test.ts +18 -0
  79. package/src/utils/__tests__/valueNormalization.test.tsx +62 -0
  80. package/src/utils/__tests__/values.test.ts +253 -0
  81. package/src/utils/applyPatch.ts +407 -0
  82. package/src/utils/bufferUntil.ts +15 -0
  83. package/src/utils/debug.ts +12 -0
  84. package/src/utils/getPortableTextMemberSchemaTypes.ts +100 -0
  85. package/src/utils/operationToPatches.ts +357 -0
  86. package/src/utils/patches.ts +36 -0
  87. package/src/utils/paths.ts +60 -0
  88. package/src/utils/ranges.ts +77 -0
  89. package/src/utils/schema.ts +8 -0
  90. package/src/utils/selection.ts +65 -0
  91. package/src/utils/ucs2Indices.ts +67 -0
  92. package/src/utils/validateValue.ts +394 -0
  93. package/src/utils/values.ts +208 -0
  94. package/src/utils/weakMaps.ts +24 -0
  95. package/src/utils/withChanges.ts +25 -0
  96. package/src/utils/withPreserveKeys.ts +14 -0
  97. package/src/utils/withoutPatching.ts +14 -0
@@ -0,0 +1,573 @@
1
+ import {
2
+ isPortableTextSpan,
3
+ type ObjectSchemaType,
4
+ type Path,
5
+ type PortableTextBlock,
6
+ type PortableTextChild,
7
+ type PortableTextObject,
8
+ type PortableTextTextBlock,
9
+ type SchemaType,
10
+ } from '@sanity/types'
11
+ import {Editor, Element as SlateElement, Node, Range, Text, Transforms} from 'slate'
12
+ import {ReactEditor} from 'slate-react'
13
+ import {type DOMNode} from 'slate-react/dist/utils/dom'
14
+
15
+ import {
16
+ type EditableAPIDeleteOptions,
17
+ type EditorSelection,
18
+ type PortableTextMemberSchemaTypes,
19
+ type PortableTextSlateEditor,
20
+ } from '../../types/editor'
21
+ import {debugWithName} from '../../utils/debug'
22
+ import {toPortableTextRange, toSlateRange} from '../../utils/ranges'
23
+ import {fromSlateValue, isEqualToEmptyEditor, toSlateValue} from '../../utils/values'
24
+ import {KEY_TO_VALUE_ELEMENT, SLATE_TO_PORTABLE_TEXT_RANGE} from '../../utils/weakMaps'
25
+ import {type PortableTextEditor} from '../PortableTextEditor'
26
+
27
+ const debug = debugWithName('API:editable')
28
+
29
+ export function createWithEditableAPI(
30
+ portableTextEditor: PortableTextEditor,
31
+ types: PortableTextMemberSchemaTypes,
32
+ keyGenerator: () => string,
33
+ ) {
34
+ return function withEditableAPI(editor: PortableTextSlateEditor): PortableTextSlateEditor {
35
+ portableTextEditor.setEditable({
36
+ focus: (): void => {
37
+ ReactEditor.focus(editor)
38
+ },
39
+ blur: (): void => {
40
+ ReactEditor.blur(editor)
41
+ },
42
+ toggleMark: (mark: string): void => {
43
+ editor.pteToggleMark(mark)
44
+ },
45
+ toggleList: (listStyle: string): void => {
46
+ editor.pteToggleListItem(listStyle)
47
+ },
48
+ toggleBlockStyle: (blockStyle: string): void => {
49
+ editor.pteToggleBlockStyle(blockStyle)
50
+ },
51
+ isMarkActive: (mark: string): boolean => {
52
+ // Try/catch this, as Slate may error because the selection is currently wrong
53
+ // TODO: catch only relevant error from Slate
54
+ try {
55
+ return editor.pteIsMarkActive(mark)
56
+ } catch (err) {
57
+ console.warn(err)
58
+ return false
59
+ }
60
+ },
61
+ marks: (): string[] => {
62
+ return (
63
+ {
64
+ ...(Editor.marks(editor) || {}),
65
+ }.marks || []
66
+ )
67
+ },
68
+ undo: (): void => editor.undo(),
69
+ redo: (): void => editor.redo(),
70
+ select: (selection: EditorSelection): void => {
71
+ const slateSelection = toSlateRange(selection, editor)
72
+ if (slateSelection) {
73
+ Transforms.select(editor, slateSelection)
74
+ } else {
75
+ Transforms.deselect(editor)
76
+ }
77
+ editor.onChange()
78
+ },
79
+ focusBlock: (): PortableTextBlock | undefined => {
80
+ if (editor.selection) {
81
+ const block = Node.descendant(editor, editor.selection.focus.path.slice(0, 1))
82
+ if (block) {
83
+ return fromSlateValue([block], types.block.name, KEY_TO_VALUE_ELEMENT.get(editor))[0]
84
+ }
85
+ }
86
+ return undefined
87
+ },
88
+ focusChild: (): PortableTextChild | undefined => {
89
+ if (editor.selection) {
90
+ const block = Node.descendant(editor, editor.selection.focus.path.slice(0, 1))
91
+ if (block && editor.isTextBlock(block)) {
92
+ const ptBlock = fromSlateValue(
93
+ [block],
94
+ types.block.name,
95
+ KEY_TO_VALUE_ELEMENT.get(editor),
96
+ )[0] as PortableTextTextBlock
97
+ return ptBlock.children[editor.selection.focus.path[1]]
98
+ }
99
+ }
100
+ return undefined
101
+ },
102
+ insertChild: (type: SchemaType, value?: {[prop: string]: any}): Path => {
103
+ if (!editor.selection) {
104
+ throw new Error('The editor has no selection')
105
+ }
106
+ const [focusBlock] = Array.from(
107
+ Editor.nodes(editor, {
108
+ at: editor.selection.focus.path.slice(0, 1),
109
+ match: (n) => n._type === types.block.name,
110
+ }),
111
+ )[0] || [undefined]
112
+ if (!focusBlock) {
113
+ throw new Error('No focused text block')
114
+ }
115
+ if (
116
+ type.name !== types.span.name &&
117
+ !types.inlineObjects.some((t) => t.name === type.name)
118
+ ) {
119
+ throw new Error('This type cannot be inserted as a child to a text block')
120
+ }
121
+ const block = toSlateValue(
122
+ [
123
+ {
124
+ _key: keyGenerator(),
125
+ _type: types.block.name,
126
+ children: [
127
+ {
128
+ _key: keyGenerator(),
129
+ _type: type.name,
130
+ ...(value ? value : {}),
131
+ },
132
+ ],
133
+ },
134
+ ],
135
+ portableTextEditor,
136
+ )[0] as unknown as SlateElement
137
+ const child = block.children[0]
138
+ const focusChildPath = editor.selection.focus.path.slice(0, 2)
139
+ const isSpanNode = child._type === types.span.name
140
+ const focusNode = Node.get(editor, focusChildPath)
141
+
142
+ // If we are inserting a span, and currently have focus on an inline object,
143
+ // move the selection to the next span (guaranteed by normalizing rules) before inserting it.
144
+ if (isSpanNode && focusNode._type !== types.span.name) {
145
+ debug('Inserting span child next to inline object child, moving selection + 1')
146
+ editor.move({distance: 1, unit: 'character'})
147
+ }
148
+
149
+ Transforms.insertNodes(editor, child, {
150
+ select: true,
151
+ at: editor.selection,
152
+ })
153
+ editor.onChange()
154
+ return (
155
+ toPortableTextRange(
156
+ fromSlateValue(editor.children, types.block.name, KEY_TO_VALUE_ELEMENT.get(editor)),
157
+ editor.selection,
158
+ types,
159
+ )?.focus.path || []
160
+ )
161
+ },
162
+ insertBlock: (type: SchemaType, value?: {[prop: string]: any}): Path => {
163
+ if (!editor.selection) {
164
+ throw new Error('The editor has no selection')
165
+ }
166
+ const block = toSlateValue(
167
+ [
168
+ {
169
+ _key: keyGenerator(),
170
+ _type: type.name,
171
+ ...(value ? value : {}),
172
+ },
173
+ ],
174
+ portableTextEditor,
175
+ )[0] as unknown as Node
176
+ const [focusBlock] = Array.from(
177
+ Editor.nodes(editor, {
178
+ at: editor.selection.focus.path.slice(0, 1),
179
+ match: (n) => n._type === types.block.name,
180
+ }),
181
+ )[0] || [undefined]
182
+
183
+ const isEmptyTextBlock = focusBlock && isEqualToEmptyEditor([focusBlock], types)
184
+
185
+ if (isEmptyTextBlock) {
186
+ // If the text block is empty, remove it before inserting the new block.
187
+ Transforms.removeNodes(editor, {at: editor.selection})
188
+ }
189
+
190
+ Editor.insertNode(editor, block)
191
+ editor.onChange()
192
+ return (
193
+ toPortableTextRange(
194
+ fromSlateValue(editor.children, types.block.name, KEY_TO_VALUE_ELEMENT.get(editor)),
195
+ editor.selection,
196
+ types,
197
+ )?.focus.path || []
198
+ )
199
+ },
200
+ hasBlockStyle: (style: string): boolean => {
201
+ try {
202
+ return editor.pteHasBlockStyle(style)
203
+ } catch (err) {
204
+ // This is fine.
205
+ // debug(err)
206
+ return false
207
+ }
208
+ },
209
+ hasListStyle: (listStyle: string): boolean => {
210
+ try {
211
+ return editor.pteHasListStyle(listStyle)
212
+ } catch (err) {
213
+ // This is fine.
214
+ // debug(err)
215
+ return false
216
+ }
217
+ },
218
+ isVoid: (element: PortableTextBlock | PortableTextChild) => {
219
+ return ![types.block.name, types.span.name].includes(element._type)
220
+ },
221
+ findByPath: (
222
+ path: Path,
223
+ ): [PortableTextBlock | PortableTextChild | undefined, Path | undefined] => {
224
+ const slatePath = toSlateRange(
225
+ {focus: {path, offset: 0}, anchor: {path, offset: 0}},
226
+ editor,
227
+ )
228
+ if (slatePath) {
229
+ const [block, blockPath] = Editor.node(editor, slatePath.focus.path.slice(0, 1))
230
+ if (block && blockPath && typeof block._key === 'string') {
231
+ if (path.length === 1 && slatePath.focus.path.length === 1) {
232
+ return [fromSlateValue([block], types.block.name)[0], [{_key: block._key}]]
233
+ }
234
+ const ptBlock = fromSlateValue(
235
+ [block],
236
+ types.block.name,
237
+ KEY_TO_VALUE_ELEMENT.get(editor),
238
+ )[0]
239
+ if (editor.isTextBlock(ptBlock)) {
240
+ const ptChild = ptBlock.children[slatePath.focus.path[1]]
241
+ if (ptChild) {
242
+ return [ptChild, [{_key: block._key}, 'children', {_key: ptChild._key}]]
243
+ }
244
+ }
245
+ }
246
+ }
247
+ return [undefined, undefined]
248
+ },
249
+ findDOMNode: (element: PortableTextBlock | PortableTextChild): DOMNode | undefined => {
250
+ let node: DOMNode | undefined
251
+ try {
252
+ const [item] = Array.from(
253
+ Editor.nodes(editor, {
254
+ at: [],
255
+ match: (n) => n._key === element._key,
256
+ }) || [],
257
+ )[0] || [undefined]
258
+ node = ReactEditor.toDOMNode(editor, item)
259
+ } catch (err) {
260
+ // Nothing
261
+ }
262
+ return node
263
+ },
264
+ activeAnnotations: (): PortableTextObject[] => {
265
+ if (!editor.selection || editor.selection.focus.path.length < 2) {
266
+ return []
267
+ }
268
+ try {
269
+ const activeAnnotations: PortableTextObject[] = []
270
+ const spans = Editor.nodes(editor, {
271
+ at: editor.selection,
272
+ match: (node) =>
273
+ Text.isText(node) &&
274
+ node.marks !== undefined &&
275
+ Array.isArray(node.marks) &&
276
+ node.marks.length > 0,
277
+ })
278
+ for (const [span, path] of spans) {
279
+ const [block] = Editor.node(editor, path, {depth: 1})
280
+ if (editor.isTextBlock(block)) {
281
+ block.markDefs?.forEach((def) => {
282
+ if (
283
+ Text.isText(span) &&
284
+ span.marks &&
285
+ Array.isArray(span.marks) &&
286
+ span.marks.includes(def._key)
287
+ ) {
288
+ activeAnnotations.push(def)
289
+ }
290
+ })
291
+ }
292
+ }
293
+ return activeAnnotations
294
+ } catch (err) {
295
+ return []
296
+ }
297
+ },
298
+ isAnnotationActive: (annotationType: PortableTextObject['_type']): boolean => {
299
+ if (!editor.selection || editor.selection.focus.path.length < 2) {
300
+ return false
301
+ }
302
+
303
+ try {
304
+ const spans = [
305
+ ...Editor.nodes(editor, {
306
+ at: editor.selection,
307
+ match: (node) => Text.isText(node),
308
+ }),
309
+ ]
310
+
311
+ if (
312
+ spans.some(
313
+ ([span]) => !isPortableTextSpan(span) || !span.marks || span.marks?.length === 0,
314
+ )
315
+ )
316
+ return false
317
+
318
+ const selectionMarkDefs = spans.reduce((accMarkDefs, [, path]) => {
319
+ const [block] = Editor.node(editor, path, {depth: 1})
320
+ if (editor.isTextBlock(block) && block.markDefs) {
321
+ return [...accMarkDefs, ...block.markDefs]
322
+ }
323
+ return accMarkDefs
324
+ }, [] as PortableTextObject[])
325
+
326
+ return spans.every(([span]) => {
327
+ if (!isPortableTextSpan(span)) return false
328
+
329
+ const spanMarkDefs = span.marks?.map(
330
+ (markKey) => selectionMarkDefs.find((def) => def?._key === markKey)?._type,
331
+ )
332
+
333
+ return spanMarkDefs?.includes(annotationType)
334
+ })
335
+ } catch (err) {
336
+ return false
337
+ }
338
+ },
339
+ addAnnotation: (
340
+ type: ObjectSchemaType,
341
+ value?: {[prop: string]: unknown},
342
+ ): {spanPath: Path; markDefPath: Path} | undefined => {
343
+ const {selection: originalSelection} = editor
344
+ let returnValue: {spanPath: Path; markDefPath: Path} | undefined = undefined
345
+ if (originalSelection) {
346
+ const [block] = Editor.node(editor, originalSelection.focus, {depth: 1})
347
+ if (!editor.isTextBlock(block)) {
348
+ return undefined
349
+ }
350
+ if (Range.isCollapsed(originalSelection)) {
351
+ editor.pteExpandToWord()
352
+ editor.onChange()
353
+ }
354
+ const [textNode] = Editor.node(editor, originalSelection.focus, {depth: 2})
355
+
356
+ // If we still have a selection, add the annotation to the selected text
357
+ if (editor.selection) {
358
+ Editor.withoutNormalizing(editor, () => {
359
+ // Add markDefs to the block
360
+ const annotationKey = keyGenerator()
361
+ Transforms.setNodes(
362
+ editor,
363
+ {
364
+ markDefs: [
365
+ ...(block.markDefs || []),
366
+ {_type: type.name, _key: annotationKey, ...value} as PortableTextObject,
367
+ ],
368
+ },
369
+ {at: originalSelection.focus},
370
+ )
371
+ editor.onChange()
372
+
373
+ // Split if needed
374
+ Transforms.setNodes(editor, {}, {match: Text.isText, split: true})
375
+ editor.onChange()
376
+
377
+ // Add marks to the span node
378
+ if (editor.selection && Text.isText(textNode)) {
379
+ Transforms.setNodes(
380
+ editor,
381
+ {
382
+ marks: [...((textNode.marks || []) as string[]), annotationKey],
383
+ },
384
+ {
385
+ at: editor.selection,
386
+ match: (n) => n._type === types.span.name,
387
+ },
388
+ )
389
+ }
390
+ editor.onChange()
391
+ if (editor.selection) {
392
+ // Insert an empty string to continue writing non-annotated text
393
+ Transforms.insertNodes(
394
+ editor,
395
+ [{_type: 'span', text: '', marks: [], _key: keyGenerator()}],
396
+ {
397
+ at: Range.end(editor.selection),
398
+ },
399
+ )
400
+ }
401
+ const newPortableTextEditorSelection = toPortableTextRange(
402
+ fromSlateValue(editor.children, types.block.name, KEY_TO_VALUE_ELEMENT.get(editor)),
403
+ editor.selection,
404
+ types,
405
+ )
406
+ if (newPortableTextEditorSelection) {
407
+ returnValue = {
408
+ spanPath: newPortableTextEditorSelection.focus.path,
409
+ markDefPath: [{_key: block._key}, 'markDefs', {_key: annotationKey}],
410
+ }
411
+ }
412
+ })
413
+ Editor.normalize(editor)
414
+ editor.onChange()
415
+ }
416
+ }
417
+ return returnValue
418
+ },
419
+ delete: (selection: EditorSelection, options?: EditableAPIDeleteOptions): void => {
420
+ if (selection) {
421
+ const range = toSlateRange(selection, editor)
422
+ const hasRange = range && range.anchor.path.length > 0 && range.focus.path.length > 0
423
+ if (!hasRange) {
424
+ throw new Error('Invalid range')
425
+ }
426
+ if (range) {
427
+ if (!options?.mode || options?.mode === 'selected') {
428
+ debug(`Deleting content in selection`)
429
+ Transforms.delete(editor, {
430
+ at: range,
431
+ hanging: true,
432
+ voids: true,
433
+ })
434
+ editor.onChange()
435
+ return
436
+ }
437
+ if (options?.mode === 'blocks') {
438
+ debug(`Deleting blocks touched by selection`)
439
+ Transforms.removeNodes(editor, {
440
+ at: range,
441
+ voids: true,
442
+ match: (node) => {
443
+ return (
444
+ editor.isTextBlock(node) ||
445
+ (!editor.isTextBlock(node) && SlateElement.isElement(node))
446
+ )
447
+ },
448
+ })
449
+ }
450
+ if (options?.mode === 'children') {
451
+ debug(`Deleting children touched by selection`)
452
+ Transforms.removeNodes(editor, {
453
+ at: range,
454
+ voids: true,
455
+ match: (node) => {
456
+ return (
457
+ node._type === types.span.name || // Text children
458
+ (!editor.isTextBlock(node) && SlateElement.isElement(node)) // inline blocks
459
+ )
460
+ },
461
+ })
462
+ }
463
+ // If the editor was emptied, insert a placeholder block
464
+ // directly into the editor's children. We don't want to do this
465
+ // through a Transform (because that would trigger a change event
466
+ // that would insert the placeholder into the actual value
467
+ // which should remain empty)
468
+ if (editor.children.length === 0) {
469
+ editor.children = [editor.pteCreateEmptyBlock()]
470
+ }
471
+ editor.onChange()
472
+ }
473
+ }
474
+ },
475
+ removeAnnotation: (type: ObjectSchemaType): void => {
476
+ let {selection} = editor
477
+ debug('Removing annotation', type)
478
+ if (selection) {
479
+ // Select the whole annotation if collapsed
480
+ if (Range.isCollapsed(selection)) {
481
+ const [node, nodePath] = Editor.node(editor, selection, {depth: 2})
482
+ if (Text.isText(node) && node.marks && typeof node.text === 'string') {
483
+ Transforms.select(editor, nodePath)
484
+ selection = editor.selection
485
+ }
486
+ }
487
+ // Do this without normalization or span references will be unstable!
488
+ Editor.withoutNormalizing(editor, () => {
489
+ if (selection && Range.isExpanded(selection)) {
490
+ selection = editor.selection
491
+ if (!selection) {
492
+ return
493
+ }
494
+ // Find the selected block, to identify the annotation to remove
495
+ const blocks = [
496
+ ...Editor.nodes(editor, {
497
+ at: selection,
498
+ match: (node) => {
499
+ return (
500
+ editor.isTextBlock(node) &&
501
+ Array.isArray(node.markDefs) &&
502
+ node.markDefs.some((def) => def._type === type.name)
503
+ )
504
+ },
505
+ }),
506
+ ]
507
+ const removedMarks: string[] = []
508
+
509
+ // Removes the marks from the text nodes
510
+ blocks.forEach(([block]) => {
511
+ if (editor.isTextBlock(block) && Array.isArray(block.markDefs)) {
512
+ const marksToRemove = block.markDefs.filter((def) => def._type === type.name)
513
+ marksToRemove.forEach((def) => {
514
+ if (!removedMarks.includes(def._key)) removedMarks.push(def._key)
515
+ Editor.removeMark(editor, def._key)
516
+ })
517
+ }
518
+ })
519
+ }
520
+ })
521
+ Editor.normalize(editor)
522
+ editor.onChange()
523
+ }
524
+ },
525
+ getSelection: (): EditorSelection | null => {
526
+ let ptRange: EditorSelection = null
527
+ if (editor.selection) {
528
+ const existing = SLATE_TO_PORTABLE_TEXT_RANGE.get(editor.selection)
529
+ if (existing) {
530
+ return existing
531
+ }
532
+ ptRange = toPortableTextRange(
533
+ fromSlateValue(editor.children, types.block.name, KEY_TO_VALUE_ELEMENT.get(editor)),
534
+ editor.selection,
535
+ types,
536
+ )
537
+ SLATE_TO_PORTABLE_TEXT_RANGE.set(editor.selection, ptRange)
538
+ }
539
+ return ptRange
540
+ },
541
+ getValue: () => {
542
+ return fromSlateValue(editor.children, types.block.name, KEY_TO_VALUE_ELEMENT.get(editor))
543
+ },
544
+ isCollapsedSelection: () => {
545
+ return !!editor.selection && Range.isCollapsed(editor.selection)
546
+ },
547
+ isExpandedSelection: () => {
548
+ return !!editor.selection && Range.isExpanded(editor.selection)
549
+ },
550
+ insertBreak: () => {
551
+ editor.insertBreak()
552
+ editor.onChange()
553
+ },
554
+ getFragment: () => {
555
+ return fromSlateValue(editor.getFragment(), types.block.name)
556
+ },
557
+ isSelectionsOverlapping: (selectionA: EditorSelection, selectionB: EditorSelection) => {
558
+ // Convert the selections to Slate ranges
559
+ const rangeA = toSlateRange(selectionA, editor)
560
+ const rangeB = toSlateRange(selectionB, editor)
561
+
562
+ // Make sure the ranges are valid
563
+ const isValidRanges = Range.isRange(rangeA) && Range.isRange(rangeB)
564
+
565
+ // Check if the ranges are overlapping
566
+ const isOverlapping = isValidRanges && Range.includes(rangeA, rangeB)
567
+
568
+ return isOverlapping
569
+ },
570
+ })
571
+ return editor
572
+ }
573
+ }