@portabletext/editor 1.0.14 → 1.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@portabletext/editor",
3
- "version": "1.0.14",
3
+ "version": "1.0.15",
4
4
  "description": "Portable Text Editor made in React",
5
5
  "keywords": [
6
6
  "sanity",
@@ -9,7 +9,7 @@
9
9
  import {isPortableTextBlock, isPortableTextSpan} from '@portabletext/toolkit'
10
10
  import {isEqual, uniq} from 'lodash'
11
11
  import {type Subject} from 'rxjs'
12
- import {type Descendant, Editor, Element, Path, Range, Text, Transforms} from 'slate'
12
+ import {type Descendant, Editor, Element, Node, Path, Range, Text, Transforms} from 'slate'
13
13
 
14
14
  import {
15
15
  type EditorChange,
@@ -54,29 +54,38 @@ export function createWithPortableTextMarkModel(
54
54
 
55
55
  // Extend Slate's default normalization. Merge spans with same set of .marks when doing merge_node operations, and clean up markDefs / marks
56
56
  editor.normalizeNode = (nodeEntry) => {
57
- normalizeNode(nodeEntry)
58
- if (
59
- editor.operations.some((op) =>
60
- [
61
- 'insert_node',
62
- 'insert_text',
63
- 'merge_node',
64
- 'remove_node',
65
- 'remove_text',
66
- 'set_node',
67
- ].includes(op.type),
68
- )
69
- ) {
70
- mergeSpans(editor)
71
- }
72
57
  const [node, path] = nodeEntry
58
+
73
59
  const isSpan = Text.isText(node) && node._type === types.span.name
74
60
  const isTextBlock = editor.isTextBlock(node)
61
+
62
+ if (editor.isTextBlock(node)) {
63
+ const children = Node.children(editor, path)
64
+
65
+ for (const [child, childPath] of children) {
66
+ const nextNode = node.children[childPath[1] + 1]
67
+
68
+ if (
69
+ editor.isTextSpan(child) &&
70
+ editor.isTextSpan(nextNode) &&
71
+ isEqual(child.marks, nextNode.marks)
72
+ ) {
73
+ debug(
74
+ 'Merging spans',
75
+ JSON.stringify(child, null, 2),
76
+ JSON.stringify(nextNode, null, 2),
77
+ )
78
+ Transforms.mergeNodes(editor, {at: [childPath[0], childPath[1] + 1], voids: true})
79
+ return
80
+ }
81
+ }
82
+ }
83
+
75
84
  if (isSpan || isTextBlock) {
76
85
  if (isSpan && !Array.isArray(node.marks)) {
77
86
  debug('Adding .marks to span node')
78
87
  Transforms.setNodes(editor, {marks: []}, {at: path})
79
- editor.onChange()
88
+ return
80
89
  }
81
90
  const hasSpanMarks = isSpan && (node.marks || []).length > 0
82
91
  if (hasSpanMarks) {
@@ -100,7 +109,7 @@ export function createWithPortableTextMarkModel(
100
109
  {marks: spanMarks.filter((mark) => !orphanedMarks.includes(mark))},
101
110
  {at: path},
102
111
  )
103
- editor.onChange()
112
+ return
104
113
  }
105
114
  }
106
115
  }
@@ -124,7 +133,7 @@ export function createWithPortableTextMarkModel(
124
133
  // eslint-disable-next-line max-depth
125
134
  if (!isNormalized) {
126
135
  Transforms.setNodes(editor, {markDefs: newMarkDefs}, {at: targetPath, voids: false})
127
- editor.onChange()
136
+ return
128
137
  }
129
138
  }
130
139
  }
@@ -148,7 +157,7 @@ export function createWithPortableTextMarkModel(
148
157
  {markDefs: uniq([...oldDefs, ...op.properties.markDefs])},
149
158
  {at: targetPath, voids: false},
150
159
  )
151
- editor.onChange()
160
+ return
152
161
  }
153
162
  }
154
163
  // Make sure marks are reset, if a block is split at the end.
@@ -169,7 +178,7 @@ export function createWithPortableTextMarkModel(
169
178
  child.marks.length > 0
170
179
  ) {
171
180
  Transforms.setNodes(editor, {marks: []}, {at: childPath, voids: false})
172
- editor.onChange()
181
+ return
173
182
  }
174
183
  }
175
184
  // Make sure markDefs are reset, if a block is split at start.
@@ -192,7 +201,7 @@ export function createWithPortableTextMarkModel(
192
201
  (!block.children[0].marks || block.children[0].marks.length === 0)
193
202
  ) {
194
203
  Transforms.setNodes(editor, {markDefs: []}, {at: blockPath})
195
- editor.onChange()
204
+ return
196
205
  }
197
206
  }
198
207
  }
@@ -203,7 +212,7 @@ export function createWithPortableTextMarkModel(
203
212
  (!node.marks || (node.marks.length > 0 && node.text === ''))
204
213
  ) {
205
214
  Transforms.setNodes(editor, {marks: []}, {at: path, voids: false})
206
- editor.onChange()
215
+ return
207
216
  }
208
217
  }
209
218
  // Check consistency of markDefs (unless we are merging two nodes)
@@ -229,9 +238,11 @@ export function createWithPortableTextMarkModel(
229
238
  },
230
239
  {at: path},
231
240
  )
232
- editor.onChange()
241
+ return
233
242
  }
234
243
  }
244
+
245
+ normalizeNode(nodeEntry)
235
246
  }
236
247
 
237
248
  editor.apply = (op) => {
@@ -353,34 +364,35 @@ export function createWithPortableTextMarkModel(
353
364
  editor.addMark = (mark: string) => {
354
365
  if (editor.selection) {
355
366
  if (Range.isExpanded(editor.selection)) {
356
- // Split if needed
357
- Transforms.setNodes(editor, {}, {match: Text.isText, split: true})
358
- // Use new selection
359
- const splitTextNodes = [
360
- ...Editor.nodes(editor, {at: editor.selection, match: Text.isText}),
361
- ]
362
- const shouldRemoveMark = splitTextNodes.every((node) => node[0].marks?.includes(mark))
363
-
364
- if (shouldRemoveMark) {
365
- editor.removeMark(mark)
366
- return editor
367
- }
368
367
  Editor.withoutNormalizing(editor, () => {
369
- splitTextNodes.forEach(([node, path]) => {
370
- const marks = [
371
- ...(Array.isArray(node.marks) ? node.marks : []).filter(
372
- (eMark: string) => eMark !== mark,
373
- ),
374
- mark,
375
- ]
376
- Transforms.setNodes(
377
- editor,
378
- {marks},
379
- {at: path, match: Text.isText, split: true, hanging: true},
380
- )
381
- })
368
+ // Split if needed
369
+ Transforms.setNodes(editor, {}, {match: Text.isText, split: true})
370
+ // Use new selection
371
+ const splitTextNodes = Range.isRange(editor.selection)
372
+ ? [...Editor.nodes(editor, {at: editor.selection, match: Text.isText})]
373
+ : []
374
+ const shouldRemoveMark =
375
+ splitTextNodes.length > 1 &&
376
+ splitTextNodes.every((node) => node[0].marks?.includes(mark))
377
+
378
+ if (shouldRemoveMark) {
379
+ editor.removeMark(mark)
380
+ } else {
381
+ splitTextNodes.forEach(([node, path]) => {
382
+ const marks = [
383
+ ...(Array.isArray(node.marks) ? node.marks : []).filter(
384
+ (eMark: string) => eMark !== mark,
385
+ ),
386
+ mark,
387
+ ]
388
+ Transforms.setNodes(
389
+ editor,
390
+ {marks},
391
+ {at: path, match: Text.isText, split: true, hanging: true},
392
+ )
393
+ })
394
+ }
382
395
  })
383
- Editor.normalize(editor)
384
396
  } else {
385
397
  const existingMarks: string[] =
386
398
  {
@@ -486,36 +498,4 @@ export function createWithPortableTextMarkModel(
486
498
  }
487
499
  return editor
488
500
  }
489
-
490
- /**
491
- * Normalize re-marked spans in selection
492
- */
493
- function mergeSpans(editor: PortableTextSlateEditor) {
494
- const {selection} = editor
495
-
496
- if (selection) {
497
- const textNodesInSelection = Array.from(
498
- Editor.nodes(editor, {
499
- at: Editor.range(editor, [selection.anchor.path[0]], [selection.focus.path[0]]),
500
- match: Text.isText,
501
- reverse: true,
502
- }),
503
- )
504
-
505
- for (const [node, path] of textNodesInSelection) {
506
- const [parent] = path.length > 1 ? Editor.node(editor, Path.parent(path)) : [undefined]
507
- const nextPath = [path[0], path[1] + 1]
508
-
509
- if (editor.isTextBlock(parent)) {
510
- const nextNode = parent.children[nextPath[1]]
511
-
512
- if (Text.isText(nextNode) && isEqual(nextNode.marks, node.marks)) {
513
- debug('Merging spans')
514
- Transforms.mergeNodes(editor, {at: nextPath, voids: true})
515
- editor.onChange()
516
- }
517
- }
518
- }
519
- }
520
- }
521
501
  }