@portabletext/editor 1.6.0 → 1.7.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 (37) hide show
  1. package/README.md +5 -0
  2. package/lib/index.d.mts +3317 -3488
  3. package/lib/index.d.ts +3317 -3488
  4. package/lib/index.esm.js +4337 -4089
  5. package/lib/index.esm.js.map +1 -1
  6. package/lib/index.js +4325 -4077
  7. package/lib/index.js.map +1 -1
  8. package/lib/index.mjs +4337 -4089
  9. package/lib/index.mjs.map +1 -1
  10. package/package.json +10 -11
  11. package/src/editor/Editable.tsx +26 -28
  12. package/src/editor/PortableTextEditor.tsx +90 -66
  13. package/src/editor/behavior/behavior.action.insert-break.ts +12 -2
  14. package/src/editor/behavior/behavior.actions.ts +51 -11
  15. package/src/editor/behavior/behavior.types.ts +23 -0
  16. package/src/editor/components/Synchronizer.tsx +11 -4
  17. package/src/editor/create-slate-editor.tsx +67 -0
  18. package/src/editor/editor-machine.ts +58 -8
  19. package/src/editor/key-generator.ts +30 -1
  20. package/src/editor/plugins/create-with-event-listeners.ts +62 -1
  21. package/src/editor/plugins/createWithEditableAPI.ts +800 -728
  22. package/src/editor/plugins/createWithMaxBlocks.ts +7 -2
  23. package/src/editor/plugins/createWithPatches.ts +4 -4
  24. package/src/editor/plugins/createWithPlaceholderBlock.ts +8 -3
  25. package/src/editor/plugins/createWithPortableTextMarkModel.ts +3 -4
  26. package/src/editor/plugins/createWithUndoRedo.ts +6 -7
  27. package/src/editor/plugins/createWithUtils.ts +2 -8
  28. package/src/editor/plugins/{index.ts → with-plugins.ts} +22 -79
  29. package/src/editor/use-editor.ts +46 -14
  30. package/src/editor/withSyncRangeDecorations.ts +20 -0
  31. package/src/index.ts +9 -1
  32. package/src/types/editor.ts +0 -1
  33. package/src/types/options.ts +1 -3
  34. package/src/utils/__tests__/operationToPatches.test.ts +7 -14
  35. package/src/utils/__tests__/patchToOperations.test.ts +4 -7
  36. package/src/editor/components/SlateContainer.tsx +0 -79
  37. package/src/editor/hooks/usePortableTextReadOnly.ts +0 -20
@@ -22,7 +22,6 @@ import type {
22
22
  EditableAPI,
23
23
  EditableAPIDeleteOptions,
24
24
  EditorSelection,
25
- PortableTextMemberSchemaTypes,
26
25
  PortableTextSlateEditor,
27
26
  } from '../../types/editor'
28
27
  import {debugWithName} from '../../utils/debug'
@@ -36,235 +35,205 @@ import {
36
35
  KEY_TO_VALUE_ELEMENT,
37
36
  SLATE_TO_PORTABLE_TEXT_RANGE,
38
37
  } from '../../utils/weakMaps'
38
+ import type {BehaviourActionImplementation} from '../behavior/behavior.actions'
39
39
  import type {EditorActor} from '../editor-machine'
40
- import type {PortableTextEditor} from '../PortableTextEditor'
41
40
  import {isDecoratorActive} from './createWithPortableTextMarkModel'
42
41
 
43
42
  const debug = debugWithName('API:editable')
44
43
 
45
- export function createWithEditableAPI(
44
+ export function createEditableAPI(
45
+ editor: PortableTextSlateEditor,
46
46
  editorActor: EditorActor,
47
- portableTextEditor: PortableTextEditor,
48
- types: PortableTextMemberSchemaTypes,
49
47
  ) {
50
- return function withEditableAPI(
51
- editor: PortableTextSlateEditor,
52
- ): PortableTextSlateEditor {
53
- portableTextEditor.setEditable({
54
- focus: (): void => {
55
- ReactEditor.focus(editor)
56
- },
57
- blur: (): void => {
58
- ReactEditor.blur(editor)
59
- },
60
- toggleMark: (mark: string): void => {
61
- editorActor.send({
62
- type: 'behavior event',
63
- behaviorEvent: {
64
- type: 'decorator.toggle',
65
- decorator: mark,
66
- },
48
+ const types = editorActor.getSnapshot().context.schema
49
+
50
+ const editableApi: EditableAPI = {
51
+ focus: (): void => {
52
+ ReactEditor.focus(editor)
53
+ },
54
+ blur: (): void => {
55
+ ReactEditor.blur(editor)
56
+ },
57
+ toggleMark: (mark: string): void => {
58
+ editorActor.send({
59
+ type: 'behavior event',
60
+ behaviorEvent: {
61
+ type: 'decorator.toggle',
62
+ decorator: mark,
63
+ },
64
+ editor,
65
+ })
66
+ },
67
+ toggleList: (listStyle: string): void => {
68
+ editor.pteToggleListItem(listStyle)
69
+ },
70
+ toggleBlockStyle: (blockStyle: string): void => {
71
+ editor.pteToggleBlockStyle(blockStyle)
72
+ },
73
+ isMarkActive: (mark: string): boolean => {
74
+ // Try/catch this, as Slate may error because the selection is currently wrong
75
+ // TODO: catch only relevant error from Slate
76
+ try {
77
+ return isDecoratorActive({editor, decorator: mark})
78
+ } catch (err) {
79
+ console.warn(err)
80
+ return false
81
+ }
82
+ },
83
+ marks: (): string[] => {
84
+ return (
85
+ {
86
+ ...(Editor.marks(editor) || {}),
87
+ }.marks || []
88
+ )
89
+ },
90
+ undo: (): void => editor.undo(),
91
+ redo: (): void => editor.redo(),
92
+ select: (selection: EditorSelection): void => {
93
+ const slateSelection = toSlateRange(selection, editor)
94
+ if (slateSelection) {
95
+ Transforms.select(editor, slateSelection)
96
+ } else {
97
+ Transforms.deselect(editor)
98
+ }
99
+ editor.onChange()
100
+ },
101
+ focusBlock: (): PortableTextBlock | undefined => {
102
+ if (editor.selection) {
103
+ const block = Node.descendant(
67
104
  editor,
68
- })
69
- },
70
- toggleList: (listStyle: string): void => {
71
- editor.pteToggleListItem(listStyle)
72
- },
73
- toggleBlockStyle: (blockStyle: string): void => {
74
- editor.pteToggleBlockStyle(blockStyle)
75
- },
76
- isMarkActive: (mark: string): boolean => {
77
- // Try/catch this, as Slate may error because the selection is currently wrong
78
- // TODO: catch only relevant error from Slate
79
- try {
80
- return isDecoratorActive({editor, decorator: mark})
81
- } catch (err) {
82
- console.warn(err)
83
- return false
84
- }
85
- },
86
- marks: (): string[] => {
87
- return (
88
- {
89
- ...(Editor.marks(editor) || {}),
90
- }.marks || []
105
+ editor.selection.focus.path.slice(0, 1),
91
106
  )
92
- },
93
- undo: (): void => editor.undo(),
94
- redo: (): void => editor.redo(),
95
- select: (selection: EditorSelection): void => {
96
- const slateSelection = toSlateRange(selection, editor)
97
- if (slateSelection) {
98
- Transforms.select(editor, slateSelection)
99
- } else {
100
- Transforms.deselect(editor)
101
- }
102
- editor.onChange()
103
- },
104
- focusBlock: (): PortableTextBlock | undefined => {
105
- if (editor.selection) {
106
- const block = Node.descendant(
107
- editor,
108
- editor.selection.focus.path.slice(0, 1),
109
- )
110
- if (block) {
111
- return fromSlateValue(
112
- [block],
113
- types.block.name,
114
- KEY_TO_VALUE_ELEMENT.get(editor),
115
- )[0]
116
- }
117
- }
118
- return undefined
119
- },
120
- focusChild: (): PortableTextChild | undefined => {
121
- if (editor.selection) {
122
- const block = Node.descendant(
123
- editor,
124
- editor.selection.focus.path.slice(0, 1),
125
- )
126
- if (block && editor.isTextBlock(block)) {
127
- const ptBlock = fromSlateValue(
128
- [block],
129
- types.block.name,
130
- KEY_TO_VALUE_ELEMENT.get(editor),
131
- )[0] as PortableTextTextBlock
132
- return ptBlock.children[editor.selection.focus.path[1]]
133
- }
134
- }
135
- return undefined
136
- },
137
- insertChild: <TSchemaType extends {name: string}>(
138
- type: TSchemaType,
139
- value?: {[prop: string]: any},
140
- ): Path => {
141
- if (!editor.selection) {
142
- throw new Error('The editor has no selection')
143
- }
144
- const [focusBlock] = Array.from(
145
- Editor.nodes(editor, {
146
- at: editor.selection.focus.path.slice(0, 1),
147
- match: (n) => n._type === types.block.name,
148
- }),
149
- )[0] || [undefined]
150
- if (!focusBlock) {
151
- throw new Error('No focused text block')
152
- }
153
- if (
154
- type.name !== types.span.name &&
155
- !types.inlineObjects.some((t) => t.name === type.name)
156
- ) {
157
- throw new Error(
158
- 'This type cannot be inserted as a child to a text block',
159
- )
160
- }
161
- const block = toSlateValue(
162
- [
163
- {
164
- _key: editorActor.getSnapshot().context.keyGenerator(),
165
- _type: types.block.name,
166
- children: [
167
- {
168
- _key: editorActor.getSnapshot().context.keyGenerator(),
169
- _type: type.name,
170
- ...(value ? value : {}),
171
- },
172
- ],
173
- },
174
- ],
175
- portableTextEditor,
176
- )[0] as unknown as SlateElement
177
- const child = block.children[0]
178
- const focusChildPath = editor.selection.focus.path.slice(0, 2)
179
- const isSpanNode = child._type === types.span.name
180
- const focusNode = Node.get(editor, focusChildPath)
181
-
182
- // If we are inserting a span, and currently have focus on an inline object,
183
- // move the selection to the next span (guaranteed by normalizing rules) before inserting it.
184
- if (isSpanNode && focusNode._type !== types.span.name) {
185
- debug(
186
- 'Inserting span child next to inline object child, moving selection + 1',
187
- )
188
- editor.move({distance: 1, unit: 'character'})
107
+ if (block) {
108
+ return fromSlateValue(
109
+ [block],
110
+ types.block.name,
111
+ KEY_TO_VALUE_ELEMENT.get(editor),
112
+ )[0]
189
113
  }
190
-
191
- Transforms.insertNodes(editor, child, {
192
- select: true,
193
- at: editor.selection,
194
- })
195
- editor.onChange()
196
- return (
197
- toPortableTextRange(
198
- fromSlateValue(
199
- editor.children,
200
- types.block.name,
201
- KEY_TO_VALUE_ELEMENT.get(editor),
202
- ),
203
- editor.selection,
204
- types,
205
- )?.focus.path || []
114
+ }
115
+ return undefined
116
+ },
117
+ focusChild: (): PortableTextChild | undefined => {
118
+ if (editor.selection) {
119
+ const block = Node.descendant(
120
+ editor,
121
+ editor.selection.focus.path.slice(0, 1),
206
122
  )
207
- },
208
- insertBlock: <TSchemaType extends {name: string}>(
209
- type: TSchemaType,
210
- value?: {[prop: string]: any},
211
- ): Path => {
212
- const block = toSlateValue(
213
- [
214
- {
215
- _key: editorActor.getSnapshot().context.keyGenerator(),
216
- _type: type.name,
217
- ...(value ? value : {}),
218
- },
219
- ],
220
- portableTextEditor,
221
- )[0] as unknown as Node
222
-
223
- if (!editor.selection) {
224
- const lastBlock = Array.from(
225
- Editor.nodes(editor, {
226
- match: (n) => !Editor.isEditor(n),
227
- at: [],
228
- reverse: true,
229
- }),
230
- )[0]
231
-
232
- // If there is no selection, let's just insert the new block at the
233
- // end of the document
234
- Editor.insertNode(editor, block)
235
-
236
- if (lastBlock && isEqualToEmptyEditor([lastBlock[0]], types)) {
237
- // And if the last block was an empty text block, let's remove
238
- // that too
239
- Transforms.removeNodes(editor, {at: lastBlock[1]})
240
- }
241
-
242
- editor.onChange()
243
-
244
- return (
245
- toPortableTextRange(
246
- fromSlateValue(
247
- editor.children,
248
- types.block.name,
249
- KEY_TO_VALUE_ELEMENT.get(editor),
250
- ),
251
- editor.selection,
252
- types,
253
- )?.focus.path ?? []
254
- )
123
+ if (block && editor.isTextBlock(block)) {
124
+ const ptBlock = fromSlateValue(
125
+ [block],
126
+ types.block.name,
127
+ KEY_TO_VALUE_ELEMENT.get(editor),
128
+ )[0] as PortableTextTextBlock
129
+ return ptBlock.children[editor.selection.focus.path[1]]
255
130
  }
131
+ }
132
+ return undefined
133
+ },
134
+ insertChild: <TSchemaType extends {name: string}>(
135
+ type: TSchemaType,
136
+ value?: {[prop: string]: any},
137
+ ): Path => {
138
+ if (!editor.selection) {
139
+ throw new Error('The editor has no selection')
140
+ }
141
+ const [focusBlock] = Array.from(
142
+ Editor.nodes(editor, {
143
+ at: editor.selection.focus.path.slice(0, 1),
144
+ match: (n) => n._type === types.block.name,
145
+ }),
146
+ )[0] || [undefined]
147
+ if (!focusBlock) {
148
+ throw new Error('No focused text block')
149
+ }
150
+ if (
151
+ type.name !== types.span.name &&
152
+ !types.inlineObjects.some((t) => t.name === type.name)
153
+ ) {
154
+ throw new Error(
155
+ 'This type cannot be inserted as a child to a text block',
156
+ )
157
+ }
158
+ const block = toSlateValue(
159
+ [
160
+ {
161
+ _key: editorActor.getSnapshot().context.keyGenerator(),
162
+ _type: types.block.name,
163
+ children: [
164
+ {
165
+ _key: editorActor.getSnapshot().context.keyGenerator(),
166
+ _type: type.name,
167
+ ...(value ? value : {}),
168
+ },
169
+ ],
170
+ },
171
+ ],
172
+ {schemaTypes: editorActor.getSnapshot().context.schema},
173
+ )[0] as unknown as SlateElement
174
+ const child = block.children[0]
175
+ const focusChildPath = editor.selection.focus.path.slice(0, 2)
176
+ const isSpanNode = child._type === types.span.name
177
+ const focusNode = Node.get(editor, focusChildPath)
178
+
179
+ // If we are inserting a span, and currently have focus on an inline object,
180
+ // move the selection to the next span (guaranteed by normalizing rules) before inserting it.
181
+ if (isSpanNode && focusNode._type !== types.span.name) {
182
+ debug(
183
+ 'Inserting span child next to inline object child, moving selection + 1',
184
+ )
185
+ editor.move({distance: 1, unit: 'character'})
186
+ }
187
+
188
+ Transforms.insertNodes(editor, child, {
189
+ select: true,
190
+ at: editor.selection,
191
+ })
192
+ editor.onChange()
193
+ return (
194
+ toPortableTextRange(
195
+ fromSlateValue(
196
+ editor.children,
197
+ types.block.name,
198
+ KEY_TO_VALUE_ELEMENT.get(editor),
199
+ ),
200
+ editor.selection,
201
+ types,
202
+ )?.focus.path || []
203
+ )
204
+ },
205
+ insertBlock: <TSchemaType extends {name: string}>(
206
+ type: TSchemaType,
207
+ value?: {[prop: string]: any},
208
+ ): Path => {
209
+ const block = toSlateValue(
210
+ [
211
+ {
212
+ _key: editorActor.getSnapshot().context.keyGenerator(),
213
+ _type: type.name,
214
+ ...(value ? value : {}),
215
+ },
216
+ ],
217
+ {schemaTypes: editorActor.getSnapshot().context.schema},
218
+ )[0] as unknown as Node
256
219
 
257
- const focusBlock = Array.from(
220
+ if (!editor.selection) {
221
+ const lastBlock = Array.from(
258
222
  Editor.nodes(editor, {
259
- at: editor.selection.focus.path.slice(0, 1),
260
- match: (n) => n._type === types.block.name,
223
+ match: (n) => !Editor.isEditor(n),
224
+ at: [],
225
+ reverse: true,
261
226
  }),
262
227
  )[0]
263
228
 
229
+ // If there is no selection, let's just insert the new block at the
230
+ // end of the document
264
231
  Editor.insertNode(editor, block)
265
232
 
266
- if (focusBlock && isEqualToEmptyEditor([focusBlock[0]], types)) {
267
- Transforms.removeNodes(editor, {at: focusBlock[1]})
233
+ if (lastBlock && isEqualToEmptyEditor([lastBlock[0]], types)) {
234
+ // And if the last block was an empty text block, let's remove
235
+ // that too
236
+ Transforms.removeNodes(editor, {at: lastBlock[1]})
268
237
  }
269
238
 
270
239
  editor.onChange()
@@ -278,572 +247,675 @@ export function createWithEditableAPI(
278
247
  ),
279
248
  editor.selection,
280
249
  types,
281
- )?.focus.path || []
250
+ )?.focus.path ?? []
282
251
  )
283
- },
284
- hasBlockStyle: (style: string): boolean => {
285
- try {
286
- return editor.pteHasBlockStyle(style)
287
- } catch {
288
- // This is fine.
289
- return false
290
- }
291
- },
292
- hasListStyle: (listStyle: string): boolean => {
293
- try {
294
- return editor.pteHasListStyle(listStyle)
295
- } catch {
296
- // This is fine.
297
- return false
298
- }
299
- },
300
- isVoid: (element: PortableTextBlock | PortableTextChild) => {
301
- return ![types.block.name, types.span.name].includes(element._type)
302
- },
303
- findByPath: (
304
- path: Path,
305
- ): [
306
- PortableTextBlock | PortableTextChild | undefined,
307
- Path | undefined,
308
- ] => {
309
- const slatePath = toSlateRange(
310
- {focus: {path, offset: 0}, anchor: {path, offset: 0}},
252
+ }
253
+
254
+ const focusBlock = Array.from(
255
+ Editor.nodes(editor, {
256
+ at: editor.selection.focus.path.slice(0, 1),
257
+ match: (n) => n._type === types.block.name,
258
+ }),
259
+ )[0]
260
+
261
+ Editor.insertNode(editor, block)
262
+
263
+ if (focusBlock && isEqualToEmptyEditor([focusBlock[0]], types)) {
264
+ Transforms.removeNodes(editor, {at: focusBlock[1]})
265
+ }
266
+
267
+ editor.onChange()
268
+
269
+ return (
270
+ toPortableTextRange(
271
+ fromSlateValue(
272
+ editor.children,
273
+ types.block.name,
274
+ KEY_TO_VALUE_ELEMENT.get(editor),
275
+ ),
276
+ editor.selection,
277
+ types,
278
+ )?.focus.path || []
279
+ )
280
+ },
281
+ hasBlockStyle: (style: string): boolean => {
282
+ try {
283
+ return editor.pteHasBlockStyle(style)
284
+ } catch {
285
+ // This is fine.
286
+ return false
287
+ }
288
+ },
289
+ hasListStyle: (listStyle: string): boolean => {
290
+ try {
291
+ return editor.pteHasListStyle(listStyle)
292
+ } catch {
293
+ // This is fine.
294
+ return false
295
+ }
296
+ },
297
+ isVoid: (element: PortableTextBlock | PortableTextChild) => {
298
+ return ![types.block.name, types.span.name].includes(element._type)
299
+ },
300
+ findByPath: (
301
+ path: Path,
302
+ ): [
303
+ PortableTextBlock | PortableTextChild | undefined,
304
+ Path | undefined,
305
+ ] => {
306
+ const slatePath = toSlateRange(
307
+ {focus: {path, offset: 0}, anchor: {path, offset: 0}},
308
+ editor,
309
+ )
310
+ if (slatePath) {
311
+ const [block, blockPath] = Editor.node(
311
312
  editor,
313
+ slatePath.focus.path.slice(0, 1),
312
314
  )
313
- if (slatePath) {
314
- const [block, blockPath] = Editor.node(
315
- editor,
316
- slatePath.focus.path.slice(0, 1),
317
- )
318
- if (block && blockPath && typeof block._key === 'string') {
319
- if (path.length === 1 && slatePath.focus.path.length === 1) {
315
+ if (block && blockPath && typeof block._key === 'string') {
316
+ if (path.length === 1 && slatePath.focus.path.length === 1) {
317
+ return [
318
+ fromSlateValue([block], types.block.name)[0],
319
+ [{_key: block._key}],
320
+ ]
321
+ }
322
+ const ptBlock = fromSlateValue(
323
+ [block],
324
+ types.block.name,
325
+ KEY_TO_VALUE_ELEMENT.get(editor),
326
+ )[0]
327
+ if (editor.isTextBlock(ptBlock)) {
328
+ const ptChild = ptBlock.children[slatePath.focus.path[1]]
329
+ if (ptChild) {
320
330
  return [
321
- fromSlateValue([block], types.block.name)[0],
322
- [{_key: block._key}],
331
+ ptChild,
332
+ [{_key: block._key}, 'children', {_key: ptChild._key}],
323
333
  ]
324
334
  }
325
- const ptBlock = fromSlateValue(
326
- [block],
327
- types.block.name,
328
- KEY_TO_VALUE_ELEMENT.get(editor),
329
- )[0]
330
- if (editor.isTextBlock(ptBlock)) {
331
- const ptChild = ptBlock.children[slatePath.focus.path[1]]
332
- if (ptChild) {
333
- return [
334
- ptChild,
335
- [{_key: block._key}, 'children', {_key: ptChild._key}],
336
- ]
337
- }
338
- }
339
335
  }
340
336
  }
341
- return [undefined, undefined]
342
- },
343
- findDOMNode: (
344
- element: PortableTextBlock | PortableTextChild,
345
- ): DOMNode | undefined => {
346
- let node: DOMNode | undefined
347
- try {
348
- const [item] = Array.from(
349
- Editor.nodes(editor, {
350
- at: [],
351
- match: (n) => n._key === element._key,
352
- }) || [],
353
- )[0] || [undefined]
354
- node = ReactEditor.toDOMNode(editor, item)
355
- } catch {
356
- // Nothing
337
+ }
338
+ return [undefined, undefined]
339
+ },
340
+ findDOMNode: (
341
+ element: PortableTextBlock | PortableTextChild,
342
+ ): DOMNode | undefined => {
343
+ let node: DOMNode | undefined
344
+ try {
345
+ const [item] = Array.from(
346
+ Editor.nodes(editor, {
347
+ at: [],
348
+ match: (n) => n._key === element._key,
349
+ }) || [],
350
+ )[0] || [undefined]
351
+ node = ReactEditor.toDOMNode(editor, item)
352
+ } catch {
353
+ // Nothing
354
+ }
355
+ return node
356
+ },
357
+ activeAnnotations: (): PortableTextObject[] => {
358
+ if (!editor.selection || editor.selection.focus.path.length < 2) {
359
+ return []
360
+ }
361
+ try {
362
+ const activeAnnotations: PortableTextObject[] = []
363
+ const spans = Editor.nodes(editor, {
364
+ at: editor.selection,
365
+ match: (node) =>
366
+ Text.isText(node) &&
367
+ node.marks !== undefined &&
368
+ Array.isArray(node.marks) &&
369
+ node.marks.length > 0,
370
+ })
371
+ for (const [span, path] of spans) {
372
+ const [block] = Editor.node(editor, path, {depth: 1})
373
+ if (editor.isTextBlock(block)) {
374
+ block.markDefs?.forEach((def) => {
375
+ if (
376
+ Text.isText(span) &&
377
+ span.marks &&
378
+ Array.isArray(span.marks) &&
379
+ span.marks.includes(def._key)
380
+ ) {
381
+ activeAnnotations.push(def)
382
+ }
383
+ })
384
+ }
357
385
  }
358
- return node
359
- },
360
- activeAnnotations: (): PortableTextObject[] => {
361
- if (!editor.selection || editor.selection.focus.path.length < 2) {
362
- return []
386
+ return activeAnnotations
387
+ } catch {
388
+ return []
389
+ }
390
+ },
391
+ isAnnotationActive: (
392
+ annotationType: PortableTextObject['_type'],
393
+ ): boolean => {
394
+ return isAnnotationActive({editor, annotation: {name: annotationType}})
395
+ },
396
+ addAnnotation: (type, value) => {
397
+ let paths: ReturnType<EditableAPI['addAnnotation']> = undefined
398
+
399
+ Editor.withoutNormalizing(editor, () => {
400
+ paths = addAnnotationActionImplementation({
401
+ context: {
402
+ keyGenerator: editorActor.getSnapshot().context.keyGenerator,
403
+ schema: types,
404
+ },
405
+ action: {
406
+ type: 'annotation.add',
407
+ annotation: {name: type.name, value: value ?? {}},
408
+ editor,
409
+ },
410
+ })
411
+ })
412
+ editor.onChange()
413
+
414
+ return paths
415
+ },
416
+ delete: (
417
+ selection: EditorSelection,
418
+ options?: EditableAPIDeleteOptions,
419
+ ): void => {
420
+ if (selection) {
421
+ const range = toSlateRange(selection, editor)
422
+ const hasRange =
423
+ range && range.anchor.path.length > 0 && range.focus.path.length > 0
424
+ if (!hasRange) {
425
+ throw new Error('Invalid range')
363
426
  }
364
- try {
365
- const activeAnnotations: PortableTextObject[] = []
366
- const spans = Editor.nodes(editor, {
367
- at: editor.selection,
368
- match: (node) =>
369
- Text.isText(node) &&
370
- node.marks !== undefined &&
371
- Array.isArray(node.marks) &&
372
- node.marks.length > 0,
373
- })
374
- for (const [span, path] of spans) {
375
- const [block] = Editor.node(editor, path, {depth: 1})
376
- if (editor.isTextBlock(block)) {
377
- block.markDefs?.forEach((def) => {
378
- if (
379
- Text.isText(span) &&
380
- span.marks &&
381
- Array.isArray(span.marks) &&
382
- span.marks.includes(def._key)
383
- ) {
384
- activeAnnotations.push(def)
385
- }
386
- })
387
- }
427
+ if (range) {
428
+ if (!options?.mode || options?.mode === 'selected') {
429
+ debug(`Deleting content in selection`)
430
+ Transforms.delete(editor, {
431
+ at: range,
432
+ hanging: true,
433
+ voids: true,
434
+ })
435
+ editor.onChange()
436
+ return
437
+ }
438
+ if (options?.mode === 'blocks') {
439
+ debug(`Deleting blocks touched by selection`)
440
+ Transforms.removeNodes(editor, {
441
+ at: range,
442
+ voids: true,
443
+ match: (node) => {
444
+ return (
445
+ editor.isTextBlock(node) ||
446
+ (!editor.isTextBlock(node) && SlateElement.isElement(node))
447
+ )
448
+ },
449
+ })
388
450
  }
389
- return activeAnnotations
390
- } catch {
391
- return []
451
+ if (options?.mode === 'children') {
452
+ debug(`Deleting children touched by selection`)
453
+ Transforms.removeNodes(editor, {
454
+ at: range,
455
+ voids: true,
456
+ match: (node) => {
457
+ return (
458
+ node._type === types.span.name || // Text children
459
+ (!editor.isTextBlock(node) && SlateElement.isElement(node)) // inline blocks
460
+ )
461
+ },
462
+ })
463
+ }
464
+ // If the editor was emptied, insert a placeholder block
465
+ // directly into the editor's children. We don't want to do this
466
+ // through a Transform (because that would trigger a change event
467
+ // that would insert the placeholder into the actual value
468
+ // which should remain empty)
469
+ if (editor.children.length === 0) {
470
+ editor.children = [editor.pteCreateTextBlock({decorators: []})]
471
+ }
472
+ editor.onChange()
392
473
  }
393
- },
394
- isAnnotationActive: (
395
- annotationType: PortableTextObject['_type'],
396
- ): boolean => {
397
- if (!editor.selection || editor.selection.focus.path.length < 2) {
398
- return false
474
+ }
475
+ },
476
+ removeAnnotation: <TSchemaType extends {name: string}>(
477
+ type: TSchemaType,
478
+ ): void => {
479
+ editorActor.send({
480
+ type: 'behavior event',
481
+ behaviorEvent: {
482
+ type: 'annotation.remove',
483
+ annotation: {name: type.name},
484
+ },
485
+ editor,
486
+ })
487
+ },
488
+ getSelection: (): EditorSelection | null => {
489
+ let ptRange: EditorSelection = null
490
+ if (editor.selection) {
491
+ const existing = SLATE_TO_PORTABLE_TEXT_RANGE.get(editor.selection)
492
+ if (existing) {
493
+ return existing
399
494
  }
495
+ ptRange = toPortableTextRange(
496
+ fromSlateValue(
497
+ editor.children,
498
+ types.block.name,
499
+ KEY_TO_VALUE_ELEMENT.get(editor),
500
+ ),
501
+ editor.selection,
502
+ types,
503
+ )
504
+ SLATE_TO_PORTABLE_TEXT_RANGE.set(editor.selection, ptRange)
505
+ }
506
+ return ptRange
507
+ },
508
+ getValue: () => {
509
+ return fromSlateValue(
510
+ editor.children,
511
+ types.block.name,
512
+ KEY_TO_VALUE_ELEMENT.get(editor),
513
+ )
514
+ },
515
+ isCollapsedSelection: () => {
516
+ return !!editor.selection && Range.isCollapsed(editor.selection)
517
+ },
518
+ isExpandedSelection: () => {
519
+ return !!editor.selection && Range.isExpanded(editor.selection)
520
+ },
521
+ insertBreak: () => {
522
+ editor.insertBreak()
523
+ editor.onChange()
524
+ },
525
+ getFragment: () => {
526
+ return fromSlateValue(editor.getFragment(), types.block.name)
527
+ },
528
+ isSelectionsOverlapping: (
529
+ selectionA: EditorSelection,
530
+ selectionB: EditorSelection,
531
+ ) => {
532
+ // Convert the selections to Slate ranges
533
+ const rangeA = toSlateRange(selectionA, editor)
534
+ const rangeB = toSlateRange(selectionB, editor)
535
+
536
+ // Make sure the ranges are valid
537
+ const isValidRanges = Range.isRange(rangeA) && Range.isRange(rangeB)
538
+
539
+ // Check if the ranges are overlapping
540
+ const isOverlapping = isValidRanges && Range.includes(rangeA, rangeB)
541
+
542
+ return isOverlapping
543
+ },
544
+ }
400
545
 
401
- try {
402
- const spans = [
403
- ...Editor.nodes(editor, {
404
- at: editor.selection,
405
- match: (node) => Text.isText(node),
406
- }),
407
- ]
408
-
409
- if (spans.length === 0) {
410
- return false
411
- }
412
-
413
- if (
414
- spans.some(
415
- ([span]) =>
416
- !isPortableTextSpan(span) ||
417
- !span.marks ||
418
- span.marks?.length === 0,
419
- )
420
- )
421
- return false
546
+ return editableApi
547
+ }
422
548
 
423
- const selectionMarkDefs = spans.reduce((accMarkDefs, [, path]) => {
424
- const [block] = Editor.node(editor, path, {depth: 1})
425
- if (editor.isTextBlock(block) && block.markDefs) {
426
- return [...accMarkDefs, ...block.markDefs]
427
- }
428
- return accMarkDefs
429
- }, [] as PortableTextObject[])
549
+ function isAnnotationActive({
550
+ editor,
551
+ annotation,
552
+ }: {
553
+ editor: PortableTextSlateEditor
554
+ annotation: {
555
+ name: string
556
+ }
557
+ }) {
558
+ if (!editor.selection || editor.selection.focus.path.length < 2) {
559
+ return false
560
+ }
430
561
 
431
- return spans.every(([span]) => {
432
- if (!isPortableTextSpan(span)) return false
562
+ try {
563
+ const spans = [
564
+ ...Editor.nodes(editor, {
565
+ at: editor.selection,
566
+ match: (node) => Text.isText(node),
567
+ }),
568
+ ]
569
+
570
+ if (spans.length === 0) {
571
+ return false
572
+ }
573
+
574
+ if (
575
+ spans.some(
576
+ ([span]) =>
577
+ !isPortableTextSpan(span) || !span.marks || span.marks?.length === 0,
578
+ )
579
+ )
580
+ return false
581
+
582
+ const selectionMarkDefs = spans.reduce((accMarkDefs, [, path]) => {
583
+ const [block] = Editor.node(editor, path, {depth: 1})
584
+ if (editor.isTextBlock(block) && block.markDefs) {
585
+ return [...accMarkDefs, ...block.markDefs]
586
+ }
587
+ return accMarkDefs
588
+ }, [] as PortableTextObject[])
589
+
590
+ return spans.every(([span]) => {
591
+ if (!isPortableTextSpan(span)) return false
592
+
593
+ const spanMarkDefs = span.marks?.map(
594
+ (markKey) =>
595
+ selectionMarkDefs.find((def) => def?._key === markKey)?._type,
596
+ )
597
+
598
+ return spanMarkDefs?.includes(annotation.name)
599
+ })
600
+ } catch {
601
+ return false
602
+ }
603
+ }
433
604
 
434
- const spanMarkDefs = span.marks?.map(
435
- (markKey) =>
436
- selectionMarkDefs.find((def) => def?._key === markKey)?._type,
437
- )
605
+ /**
606
+ * @public
607
+ */
608
+ export type AddedAnnotationPaths = {
609
+ /**
610
+ * @deprecated An annotation may be applied to multiple blocks, resulting
611
+ * in multiple `markDef`'s being created. Use `markDefPaths` instead.
612
+ */
613
+ markDefPath: Path
614
+ markDefPaths: Array<Path>
615
+ /**
616
+ * @deprecated Does not return anything meaningful since an annotation
617
+ * can span multiple blocks and spans. If references the span closest
618
+ * to the focus point of the selection.
619
+ */
620
+ spanPath: Path
621
+ }
438
622
 
439
- return spanMarkDefs?.includes(annotationType)
440
- })
441
- } catch {
442
- return false
623
+ export const addAnnotationActionImplementation: BehaviourActionImplementation<
624
+ 'annotation.add',
625
+ AddedAnnotationPaths | undefined
626
+ > = ({context, action}) => {
627
+ const editor = action.editor
628
+ const {selection: originalSelection} = editor
629
+ let paths: AddedAnnotationPaths | undefined = undefined
630
+
631
+ if (originalSelection) {
632
+ if (Range.isCollapsed(originalSelection)) {
633
+ editor.pteExpandToWord()
634
+ editor.onChange()
635
+ }
636
+
637
+ // If we still have a selection, add the annotation to the selected text
638
+ if (editor.selection) {
639
+ let spanPath: Path | undefined
640
+ let markDefPath: Path | undefined
641
+ const markDefPaths: Path[] = []
642
+
643
+ if (!editor.selection) {
644
+ return
645
+ }
646
+
647
+ const selectedBlocks = Editor.nodes(editor, {
648
+ at: editor.selection,
649
+ match: (node) => editor.isTextBlock(node),
650
+ reverse: Range.isBackward(editor.selection),
651
+ })
652
+
653
+ for (const [block, blockPath] of selectedBlocks) {
654
+ if (block.children.length === 0) {
655
+ continue
443
656
  }
444
- },
445
- addAnnotation: (type, value) => {
446
- const {selection: originalSelection} = editor
447
- let returnValue: ReturnType<EditableAPI['addAnnotation']> | undefined =
448
- undefined
449
-
450
- if (originalSelection) {
451
- if (Range.isCollapsed(originalSelection)) {
452
- editor.pteExpandToWord()
453
- editor.onChange()
454
- }
455
657
 
456
- // If we still have a selection, add the annotation to the selected text
457
- if (editor.selection) {
458
- let spanPath: Path | undefined
459
- let markDefPath: Path | undefined
460
- const markDefPaths: Path[] = []
461
-
462
- Editor.withoutNormalizing(editor, () => {
463
- if (!editor.selection) {
464
- return
465
- }
466
-
467
- const selectedBlocks = Editor.nodes(editor, {
468
- at: editor.selection,
469
- match: (node) => editor.isTextBlock(node),
470
- reverse: Range.isBackward(editor.selection),
471
- })
472
-
473
- for (const [block, blockPath] of selectedBlocks) {
474
- if (block.children.length === 0) {
475
- continue
476
- }
477
-
478
- if (
479
- block.children.length === 1 &&
480
- block.children[0].text === ''
481
- ) {
482
- continue
483
- }
484
-
485
- const annotationKey = editorActor
486
- .getSnapshot()
487
- .context.keyGenerator()
488
- const markDefs = block.markDefs ?? []
489
- const existingMarkDef = markDefs.find(
490
- (markDef) =>
491
- markDef._type === type.name &&
492
- markDef._key === annotationKey,
493
- )
494
-
495
- if (existingMarkDef === undefined) {
496
- Transforms.setNodes(
497
- editor,
498
- {
499
- markDefs: [
500
- ...markDefs,
501
- {
502
- _type: type.name,
503
- _key: annotationKey,
504
- ...value,
505
- },
506
- ],
507
- },
508
- {at: blockPath},
509
- )
510
-
511
- markDefPath = [
512
- {_key: block._key},
513
- 'markDefs',
514
- {_key: annotationKey},
515
- ]
516
- if (Range.isBackward(editor.selection)) {
517
- markDefPaths.unshift(markDefPath)
518
- } else {
519
- markDefPaths.push(markDefPath)
520
- }
521
- }
522
-
523
- Transforms.setNodes(
524
- editor,
525
- {},
526
- {match: Text.isText, split: true},
527
- )
658
+ if (block.children.length === 1 && block.children[0].text === '') {
659
+ continue
660
+ }
528
661
 
529
- const children = Node.children(editor, blockPath)
530
-
531
- for (const [span, path] of children) {
532
- if (!editor.isTextSpan(span)) {
533
- continue
534
- }
535
-
536
- if (!Range.includes(editor.selection, path)) {
537
- continue
538
- }
539
-
540
- const marks = span.marks ?? []
541
- const existingSameTypeAnnotations = marks.filter((mark) =>
542
- markDefs.some(
543
- (markDef) =>
544
- markDef._key === mark && markDef._type === type.name,
545
- ),
546
- )
547
-
548
- Transforms.setNodes(
549
- editor,
550
- {
551
- marks: [
552
- ...marks.filter(
553
- (mark) => !existingSameTypeAnnotations.includes(mark),
554
- ),
555
- annotationKey,
556
- ],
557
- },
558
- {at: path},
559
- )
560
- spanPath = [{_key: block._key}, 'children', {_key: span._key}]
561
- }
562
- }
662
+ const annotationKey = context.keyGenerator()
663
+ const markDefs = block.markDefs ?? []
664
+ const existingMarkDef = markDefs.find(
665
+ (markDef) =>
666
+ markDef._type === action.annotation.name &&
667
+ markDef._key === annotationKey,
668
+ )
563
669
 
564
- if (markDefPath && spanPath) {
565
- returnValue = {
566
- markDefPath,
567
- markDefPaths,
568
- spanPath,
569
- }
570
- }
571
- })
572
- editor.onChange()
573
- }
574
- }
575
- return returnValue
576
- },
577
- delete: (
578
- selection: EditorSelection,
579
- options?: EditableAPIDeleteOptions,
580
- ): void => {
581
- if (selection) {
582
- const range = toSlateRange(selection, editor)
583
- const hasRange =
584
- range && range.anchor.path.length > 0 && range.focus.path.length > 0
585
- if (!hasRange) {
586
- throw new Error('Invalid range')
587
- }
588
- if (range) {
589
- if (!options?.mode || options?.mode === 'selected') {
590
- debug(`Deleting content in selection`)
591
- Transforms.delete(editor, {
592
- at: range,
593
- hanging: true,
594
- voids: true,
595
- })
596
- editor.onChange()
597
- return
598
- }
599
- if (options?.mode === 'blocks') {
600
- debug(`Deleting blocks touched by selection`)
601
- Transforms.removeNodes(editor, {
602
- at: range,
603
- voids: true,
604
- match: (node) => {
605
- return (
606
- editor.isTextBlock(node) ||
607
- (!editor.isTextBlock(node) && SlateElement.isElement(node))
608
- )
609
- },
610
- })
611
- }
612
- if (options?.mode === 'children') {
613
- debug(`Deleting children touched by selection`)
614
- Transforms.removeNodes(editor, {
615
- at: range,
616
- voids: true,
617
- match: (node) => {
618
- return (
619
- node._type === types.span.name || // Text children
620
- (!editor.isTextBlock(node) && SlateElement.isElement(node)) // inline blocks
621
- )
670
+ if (existingMarkDef === undefined) {
671
+ Transforms.setNodes(
672
+ editor,
673
+ {
674
+ markDefs: [
675
+ ...markDefs,
676
+ {
677
+ _type: action.annotation.name,
678
+ _key: annotationKey,
679
+ ...action.annotation.value,
622
680
  },
623
- })
624
- }
625
- // If the editor was emptied, insert a placeholder block
626
- // directly into the editor's children. We don't want to do this
627
- // through a Transform (because that would trigger a change event
628
- // that would insert the placeholder into the actual value
629
- // which should remain empty)
630
- if (editor.children.length === 0) {
631
- editor.children = [editor.pteCreateTextBlock({decorators: []})]
632
- }
633
- editor.onChange()
681
+ ],
682
+ },
683
+ {at: blockPath},
684
+ )
685
+
686
+ markDefPath = [{_key: block._key}, 'markDefs', {_key: annotationKey}]
687
+ if (Range.isBackward(editor.selection)) {
688
+ markDefPaths.unshift(markDefPath)
689
+ } else {
690
+ markDefPaths.push(markDefPath)
634
691
  }
635
692
  }
636
- },
637
- removeAnnotation: <TSchemaType extends {name: string}>(
638
- type: TSchemaType,
639
- ): void => {
640
- debug('Removing annotation', type)
641
693
 
642
- Editor.withoutNormalizing(editor, () => {
643
- if (!editor.selection) {
644
- return
645
- }
694
+ Transforms.setNodes(editor, {}, {match: Text.isText, split: true})
646
695
 
647
- if (Range.isCollapsed(editor.selection)) {
648
- const [block, blockPath] = Editor.node(editor, editor.selection, {
649
- depth: 1,
650
- })
696
+ const children = Node.children(editor, blockPath)
651
697
 
652
- if (!editor.isTextBlock(block)) {
653
- return
654
- }
698
+ for (const [span, path] of children) {
699
+ if (!editor.isTextSpan(span)) {
700
+ continue
701
+ }
655
702
 
656
- const markDefs = block.markDefs ?? []
657
- const potentialAnnotations = markDefs.filter(
658
- (markDef) => markDef._type === type.name,
659
- )
703
+ if (!Range.includes(editor.selection, path)) {
704
+ continue
705
+ }
660
706
 
661
- const [selectedChild, selectedChildPath] = Editor.node(
662
- editor,
663
- editor.selection,
664
- {
665
- depth: 2,
666
- },
667
- )
707
+ const marks = span.marks ?? []
708
+ const existingSameTypeAnnotations = marks.filter((mark) =>
709
+ markDefs.some(
710
+ (markDef) =>
711
+ markDef._key === mark &&
712
+ markDef._type === action.annotation.name,
713
+ ),
714
+ )
668
715
 
669
- if (!editor.isTextSpan(selectedChild)) {
670
- return
671
- }
716
+ Transforms.setNodes(
717
+ editor,
718
+ {
719
+ marks: [
720
+ ...marks.filter(
721
+ (mark) => !existingSameTypeAnnotations.includes(mark),
722
+ ),
723
+ annotationKey,
724
+ ],
725
+ },
726
+ {at: path},
727
+ )
728
+ spanPath = [{_key: block._key}, 'children', {_key: span._key}]
729
+ }
730
+ }
672
731
 
673
- const annotationToRemove = selectedChild.marks?.find((mark) =>
674
- potentialAnnotations.some((markDef) => markDef._key === mark),
675
- )
732
+ if (markDefPath && spanPath) {
733
+ paths = {
734
+ markDefPath,
735
+ markDefPaths,
736
+ spanPath,
737
+ }
738
+ }
739
+ }
740
+ }
741
+ return paths
742
+ }
676
743
 
677
- if (!annotationToRemove) {
678
- return
679
- }
744
+ export const removeAnnotationActionImplementation: BehaviourActionImplementation<
745
+ 'annotation.remove'
746
+ > = ({action}) => {
747
+ const editor = action.editor
680
748
 
681
- const previousSpansWithSameAnnotation: Array<
682
- [span: PortableTextSpan, path: SlatePath]
683
- > = []
749
+ debug('Removing annotation', action.annotation.name)
684
750
 
685
- for (const [child, childPath] of Node.children(editor, blockPath, {
686
- reverse: true,
687
- })) {
688
- if (!editor.isTextSpan(child)) {
689
- continue
690
- }
751
+ if (!editor.selection) {
752
+ return
753
+ }
691
754
 
692
- if (!SlatePath.isBefore(childPath, selectedChildPath)) {
693
- continue
694
- }
755
+ if (Range.isCollapsed(editor.selection)) {
756
+ const [block, blockPath] = Editor.node(editor, editor.selection, {
757
+ depth: 1,
758
+ })
695
759
 
696
- if (child.marks?.includes(annotationToRemove)) {
697
- previousSpansWithSameAnnotation.push([child, childPath])
698
- } else {
699
- break
700
- }
701
- }
760
+ if (!editor.isTextBlock(block)) {
761
+ return
762
+ }
702
763
 
703
- const nextSpansWithSameAnnotation: Array<
704
- [span: PortableTextSpan, path: SlatePath]
705
- > = []
764
+ const markDefs = block.markDefs ?? []
765
+ const potentialAnnotations = markDefs.filter(
766
+ (markDef) => markDef._type === action.annotation.name,
767
+ )
706
768
 
707
- for (const [child, childPath] of Node.children(editor, blockPath)) {
708
- if (!editor.isTextSpan(child)) {
709
- continue
710
- }
769
+ const [selectedChild, selectedChildPath] = Editor.node(
770
+ editor,
771
+ editor.selection,
772
+ {
773
+ depth: 2,
774
+ },
775
+ )
776
+
777
+ if (!editor.isTextSpan(selectedChild)) {
778
+ return
779
+ }
780
+
781
+ const annotationToRemove = selectedChild.marks?.find((mark) =>
782
+ potentialAnnotations.some((markDef) => markDef._key === mark),
783
+ )
784
+
785
+ if (!annotationToRemove) {
786
+ return
787
+ }
788
+
789
+ const previousSpansWithSameAnnotation: Array<
790
+ [span: PortableTextSpan, path: SlatePath]
791
+ > = []
792
+
793
+ for (const [child, childPath] of Node.children(editor, blockPath, {
794
+ reverse: true,
795
+ })) {
796
+ if (!editor.isTextSpan(child)) {
797
+ continue
798
+ }
799
+
800
+ if (!SlatePath.isBefore(childPath, selectedChildPath)) {
801
+ continue
802
+ }
803
+
804
+ if (child.marks?.includes(annotationToRemove)) {
805
+ previousSpansWithSameAnnotation.push([child, childPath])
806
+ } else {
807
+ break
808
+ }
809
+ }
810
+
811
+ const nextSpansWithSameAnnotation: Array<
812
+ [span: PortableTextSpan, path: SlatePath]
813
+ > = []
814
+
815
+ for (const [child, childPath] of Node.children(editor, blockPath)) {
816
+ if (!editor.isTextSpan(child)) {
817
+ continue
818
+ }
819
+
820
+ if (!SlatePath.isAfter(childPath, selectedChildPath)) {
821
+ continue
822
+ }
823
+
824
+ if (child.marks?.includes(annotationToRemove)) {
825
+ nextSpansWithSameAnnotation.push([child, childPath])
826
+ } else {
827
+ break
828
+ }
829
+ }
830
+
831
+ for (const [child, childPath] of [
832
+ ...previousSpansWithSameAnnotation,
833
+ [selectedChild, selectedChildPath] as const,
834
+ ...nextSpansWithSameAnnotation,
835
+ ]) {
836
+ Transforms.setNodes(
837
+ editor,
838
+ {
839
+ marks: child.marks?.filter((mark) => mark !== annotationToRemove),
840
+ },
841
+ {at: childPath},
842
+ )
843
+ }
844
+ } else {
845
+ Transforms.setNodes(
846
+ editor,
847
+ {},
848
+ {
849
+ match: (node) => editor.isTextSpan(node),
850
+ split: true,
851
+ hanging: true,
852
+ },
853
+ )
711
854
 
712
- if (!SlatePath.isAfter(childPath, selectedChildPath)) {
713
- continue
714
- }
855
+ const blocks = Editor.nodes(editor, {
856
+ at: editor.selection,
857
+ match: (node) => editor.isTextBlock(node),
858
+ })
715
859
 
716
- if (child.marks?.includes(annotationToRemove)) {
717
- nextSpansWithSameAnnotation.push([child, childPath])
718
- } else {
719
- break
720
- }
721
- }
860
+ for (const [block, blockPath] of blocks) {
861
+ const children = Node.children(editor, blockPath)
722
862
 
723
- for (const [child, childPath] of [
724
- ...previousSpansWithSameAnnotation,
725
- [selectedChild, selectedChildPath] as const,
726
- ...nextSpansWithSameAnnotation,
727
- ]) {
728
- Transforms.setNodes(
729
- editor,
730
- {
731
- marks: child.marks?.filter(
732
- (mark) => mark !== annotationToRemove,
733
- ),
734
- },
735
- {at: childPath},
736
- )
737
- }
738
- } else {
739
- Transforms.setNodes(
740
- editor,
741
- {},
742
- {
743
- match: (node) => editor.isTextSpan(node),
744
- split: true,
745
- hanging: true,
746
- },
747
- )
863
+ for (const [child, childPath] of children) {
864
+ if (!editor.isTextSpan(child)) {
865
+ continue
866
+ }
748
867
 
749
- const blocks = Editor.nodes(editor, {
750
- at: editor.selection,
751
- match: (node) => editor.isTextBlock(node),
752
- })
868
+ if (!Range.includes(editor.selection, childPath)) {
869
+ continue
870
+ }
753
871
 
754
- for (const [block, blockPath] of blocks) {
755
- const children = Node.children(editor, blockPath)
756
-
757
- for (const [child, childPath] of children) {
758
- if (!editor.isTextSpan(child)) {
759
- continue
760
- }
761
-
762
- if (!Range.includes(editor.selection, childPath)) {
763
- continue
764
- }
765
-
766
- const markDefs = block.markDefs ?? []
767
- const marks = child.marks ?? []
768
- const marksWithoutAnnotation = marks.filter((mark) => {
769
- const markDef = markDefs.find(
770
- (markDef) => markDef._key === mark,
771
- )
772
- return markDef?._type !== type.name
773
- })
774
-
775
- if (marksWithoutAnnotation.length !== marks.length) {
776
- Transforms.setNodes(
777
- editor,
778
- {
779
- marks: marksWithoutAnnotation,
780
- },
781
- {at: childPath},
782
- )
783
- }
784
- }
785
- }
786
- }
872
+ const markDefs = block.markDefs ?? []
873
+ const marks = child.marks ?? []
874
+ const marksWithoutAnnotation = marks.filter((mark) => {
875
+ const markDef = markDefs.find((markDef) => markDef._key === mark)
876
+ return markDef?._type !== action.annotation.name
787
877
  })
788
- editor.onChange()
789
- },
790
- getSelection: (): EditorSelection | null => {
791
- let ptRange: EditorSelection = null
792
- if (editor.selection) {
793
- const existing = SLATE_TO_PORTABLE_TEXT_RANGE.get(editor.selection)
794
- if (existing) {
795
- return existing
796
- }
797
- ptRange = toPortableTextRange(
798
- fromSlateValue(
799
- editor.children,
800
- types.block.name,
801
- KEY_TO_VALUE_ELEMENT.get(editor),
802
- ),
803
- editor.selection,
804
- types,
878
+
879
+ if (marksWithoutAnnotation.length !== marks.length) {
880
+ Transforms.setNodes(
881
+ editor,
882
+ {
883
+ marks: marksWithoutAnnotation,
884
+ },
885
+ {at: childPath},
805
886
  )
806
- SLATE_TO_PORTABLE_TEXT_RANGE.set(editor.selection, ptRange)
807
887
  }
808
- return ptRange
809
- },
810
- getValue: () => {
811
- return fromSlateValue(
812
- editor.children,
813
- types.block.name,
814
- KEY_TO_VALUE_ELEMENT.get(editor),
815
- )
816
- },
817
- isCollapsedSelection: () => {
818
- return !!editor.selection && Range.isCollapsed(editor.selection)
819
- },
820
- isExpandedSelection: () => {
821
- return !!editor.selection && Range.isExpanded(editor.selection)
822
- },
823
- insertBreak: () => {
824
- editor.insertBreak()
825
- editor.onChange()
826
- },
827
- getFragment: () => {
828
- return fromSlateValue(editor.getFragment(), types.block.name)
829
- },
830
- isSelectionsOverlapping: (
831
- selectionA: EditorSelection,
832
- selectionB: EditorSelection,
833
- ) => {
834
- // Convert the selections to Slate ranges
835
- const rangeA = toSlateRange(selectionA, editor)
836
- const rangeB = toSlateRange(selectionB, editor)
837
-
838
- // Make sure the ranges are valid
839
- const isValidRanges = Range.isRange(rangeA) && Range.isRange(rangeB)
840
-
841
- // Check if the ranges are overlapping
842
- const isOverlapping = isValidRanges && Range.includes(rangeA, rangeB)
888
+ }
889
+ }
890
+ }
891
+ }
843
892
 
844
- return isOverlapping
893
+ export const toggleAnnotationActionImplementation: BehaviourActionImplementation<
894
+ 'annotation.toggle',
895
+ AddedAnnotationPaths | undefined
896
+ > = ({context, action}) => {
897
+ const isActive = isAnnotationActive({
898
+ editor: action.editor,
899
+ annotation: {name: action.annotation.name},
900
+ })
901
+
902
+ if (isActive) {
903
+ removeAnnotationActionImplementation({
904
+ context,
905
+ action: {
906
+ type: 'annotation.remove',
907
+ annotation: action.annotation,
908
+ editor: action.editor,
909
+ },
910
+ })
911
+ } else {
912
+ return addAnnotationActionImplementation({
913
+ context,
914
+ action: {
915
+ type: 'annotation.add',
916
+ annotation: action.annotation,
917
+ editor: action.editor,
845
918
  },
846
919
  })
847
- return editor
848
920
  }
849
921
  }