@portabletext/editor 1.0.13 → 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.13",
3
+ "version": "1.0.15",
4
4
  "description": "Portable Text Editor made in React",
5
5
  "keywords": [
6
6
  "sanity",
@@ -47,22 +47,22 @@
47
47
  "is-hotkey-esm": "^1.0.0",
48
48
  "lodash": "^4.17.21",
49
49
  "slate": "0.103.0",
50
- "slate-react": "0.107.1"
50
+ "slate-react": "0.108.0"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@jest/globals": "^29.7.0",
54
54
  "@playwright/test": "1.46.0",
55
55
  "@portabletext/toolkit": "^2.0.15",
56
- "@sanity/block-tools": "^3.53.0",
56
+ "@sanity/block-tools": "^3.54.0",
57
57
  "@sanity/diff-match-patch": "^3.1.1",
58
58
  "@sanity/eslint-config-i18n": "^1.1.0",
59
59
  "@sanity/eslint-config-studio": "^4.0.0",
60
60
  "@sanity/pkg-utils": "^6.10.9",
61
- "@sanity/schema": "^3.53.0",
61
+ "@sanity/schema": "^3.54.0",
62
62
  "@sanity/test": "0.0.1-alpha.1",
63
- "@sanity/types": "^3.53.0",
63
+ "@sanity/types": "^3.54.0",
64
64
  "@sanity/ui": "^2.8.8",
65
- "@sanity/util": "^3.53.0",
65
+ "@sanity/util": "^3.54.0",
66
66
  "@testing-library/dom": "^10.4.0",
67
67
  "@testing-library/react": "^16.0.0",
68
68
  "@types/debug": "^4.1.5",
@@ -74,8 +74,8 @@
74
74
  "@types/react": "^18.3.3",
75
75
  "@types/react-dom": "^18.3.0",
76
76
  "@types/ws": "~8.5.12",
77
- "@typescript-eslint/eslint-plugin": "^8.0.1",
78
- "@typescript-eslint/parser": "^8.0.1",
77
+ "@typescript-eslint/eslint-plugin": "^8.1.0",
78
+ "@typescript-eslint/parser": "^8.1.0",
79
79
  "@vitejs/plugin-react": "^4.3.1",
80
80
  "dotenv": "^16.4.5",
81
81
  "eslint": "^8.57.0",
@@ -84,10 +84,10 @@
84
84
  "eslint-import-resolver-typescript": "^3.6.1",
85
85
  "eslint-plugin-import": "^2.29.1",
86
86
  "eslint-plugin-prettier": "^5.2.1",
87
- "eslint-plugin-react-compiler": "0.0.0-experimental-9ed098e-20240725",
87
+ "eslint-plugin-react-compiler": "0.0.0-experimental-d0e920e-20240815",
88
88
  "eslint-plugin-tsdoc": "^0.3.0",
89
89
  "eslint-plugin-unicorn": "^55.0.0",
90
- "eslint-plugin-unused-imports": "^4.0.1",
90
+ "eslint-plugin-unused-imports": "^4.1.3",
91
91
  "express": "^4.19.2",
92
92
  "express-ws": "^5.0.2",
93
93
  "jest": "^29.7.0",
@@ -99,9 +99,9 @@
99
99
  "react-dom": "^18.3.1",
100
100
  "rxjs": "^7.8.1",
101
101
  "styled-components": "^6.1.12",
102
- "tsx": "^4.16.5",
102
+ "tsx": "^4.17.0",
103
103
  "typescript": "5.5.4",
104
- "vite": "^5.3.5"
104
+ "vite": "^5.4.1"
105
105
  },
106
106
  "peerDependencies": {
107
107
  "@sanity/block-tools": "^3.47.1",
@@ -11,7 +11,6 @@ import {validateValue} from '../../utils/validateValue'
11
11
  import {toSlateValue, VOID_CHILD_KEY} from '../../utils/values'
12
12
  import {isChangingLocally, isChangingRemotely, withRemoteChanges} from '../../utils/withChanges'
13
13
  import {withoutPatching} from '../../utils/withoutPatching'
14
- import {withPreserveKeys} from '../../utils/withPreserveKeys'
15
14
  import {withoutSaving} from '../plugins/createWithUndoRedo'
16
15
  import {type PortableTextEditor} from '../PortableTextEditor'
17
16
 
@@ -184,10 +183,8 @@ export function useSyncValue(
184
183
  currentBlock,
185
184
  )
186
185
  if (validation.valid || validation.resolution?.autoResolve) {
187
- withPreserveKeys(slateEditor, () => {
188
- Transforms.insertNodes(slateEditor, currentBlock, {
189
- at: [currentBlockIndex],
190
- })
186
+ Transforms.insertNodes(slateEditor, currentBlock, {
187
+ at: [currentBlockIndex],
191
188
  })
192
189
  } else {
193
190
  debug('Invalid', validation)
@@ -267,9 +264,7 @@ function _replaceBlock(
267
264
  Transforms.deselect(slateEditor)
268
265
  }
269
266
  Transforms.removeNodes(slateEditor, {at: [currentBlockIndex]})
270
- withPreserveKeys(slateEditor, () => {
271
- Transforms.insertNodes(slateEditor, currentBlock, {at: [currentBlockIndex]})
272
- })
267
+ Transforms.insertNodes(slateEditor, currentBlock, {at: [currentBlockIndex]})
273
268
  slateEditor.onChange()
274
269
  if (selectionFocusOnBlock) {
275
270
  Transforms.select(slateEditor, currentSelection)
@@ -350,21 +345,17 @@ function _updateBlock(
350
345
  Transforms.removeNodes(slateEditor, {
351
346
  at: [currentBlockIndex, currentBlockChildIndex],
352
347
  })
353
- withPreserveKeys(slateEditor, () => {
354
- Transforms.insertNodes(slateEditor, currentBlockChild as Node, {
355
- at: [currentBlockIndex, currentBlockChildIndex],
356
- })
348
+ Transforms.insertNodes(slateEditor, currentBlockChild as Node, {
349
+ at: [currentBlockIndex, currentBlockChildIndex],
357
350
  })
358
351
  slateEditor.onChange()
359
352
  // Insert it if it didn't exist before
360
353
  } else if (!oldBlockChild) {
361
354
  debug('Inserting new child', currentBlockChild)
362
- withPreserveKeys(slateEditor, () => {
363
- Transforms.insertNodes(slateEditor, currentBlockChild as Node, {
364
- at: [currentBlockIndex, currentBlockChildIndex],
365
- })
366
- slateEditor.onChange()
355
+ Transforms.insertNodes(slateEditor, currentBlockChild as Node, {
356
+ at: [currentBlockIndex, currentBlockChildIndex],
367
357
  })
358
+ slateEditor.onChange()
368
359
  }
369
360
  }
370
361
  })
@@ -1,4 +1,6 @@
1
1
  import {type PortableTextSlateEditor} from '../../types/editor'
2
+ import {isChangingRemotely} from '../../utils/withChanges'
3
+ import {isRedoing, isUndoing} from '../../utils/withUndoRedo'
2
4
 
3
5
  /**
4
6
  * This plugin makes sure that the PTE maxBlocks prop is respected
@@ -8,6 +10,24 @@ export function createWithMaxBlocks(maxBlocks: number) {
8
10
  return function withMaxBlocks(editor: PortableTextSlateEditor): PortableTextSlateEditor {
9
11
  const {apply} = editor
10
12
  editor.apply = (operation) => {
13
+ /**
14
+ * We don't want to run any side effects when the editor is processing
15
+ * remote changes.
16
+ */
17
+ if (isChangingRemotely(editor)) {
18
+ apply(operation)
19
+ return
20
+ }
21
+
22
+ /**
23
+ * We don't want to run any side effects when the editor is undoing or
24
+ * redoing operations.
25
+ */
26
+ if (isUndoing(editor) || isRedoing(editor)) {
27
+ apply(operation)
28
+ return
29
+ }
30
+
11
31
  const rows = maxBlocks
12
32
  if (rows > 0 && editor.children.length >= rows) {
13
33
  if (
@@ -1,7 +1,8 @@
1
1
  import {Editor, Element, Node, Transforms} from 'slate'
2
2
 
3
3
  import {type PortableTextMemberSchemaTypes, type PortableTextSlateEditor} from '../../types/editor'
4
- import {isPreservingKeys, PRESERVE_KEYS} from '../../utils/withPreserveKeys'
4
+ import {isChangingRemotely} from '../../utils/withChanges'
5
+ import {isRedoing, isUndoing} from '../../utils/withUndoRedo'
5
6
 
6
7
  /**
7
8
  * This plugin makes sure that every new node in the editor get a new _key prop when created
@@ -12,23 +13,36 @@ export function createWithObjectKeys(
12
13
  keyGenerator: () => string,
13
14
  ) {
14
15
  return function withKeys(editor: PortableTextSlateEditor): PortableTextSlateEditor {
15
- PRESERVE_KEYS.set(editor, false)
16
16
  const {apply, normalizeNode} = editor
17
17
 
18
- // The apply function can be called with a scope (withPreserveKeys) that will
19
- // preserve keys for the produced nodes if they have a _key property set already.
20
18
  // The default behavior is to always generate a new key here.
21
19
  // For example, when undoing and redoing we want to retain the keys, but
22
20
  // when we create a new bold span by splitting a non-bold-span we want the produced node to get a new key.
23
21
  editor.apply = (operation) => {
24
- if (operation.type === 'split_node') {
25
- const withNewKey = !isPreservingKeys(editor) || !('_key' in operation.properties)
22
+ /**
23
+ * We don't want to run any side effects when the editor is processing
24
+ * remote changes.
25
+ */
26
+ if (isChangingRemotely(editor)) {
27
+ apply(operation)
28
+ return
29
+ }
30
+
31
+ /**
32
+ * We don't want to run any side effects when the editor is undoing or
33
+ * redoing operations.
34
+ */
35
+ if (isUndoing(editor) || isRedoing(editor)) {
36
+ apply(operation)
37
+ return
38
+ }
26
39
 
40
+ if (operation.type === 'split_node') {
27
41
  apply({
28
42
  ...operation,
29
43
  properties: {
30
44
  ...operation.properties,
31
- ...(withNewKey ? {_key: keyGenerator()} : {}),
45
+ _key: keyGenerator(),
32
46
  },
33
47
  })
34
48
 
@@ -36,15 +50,12 @@ export function createWithObjectKeys(
36
50
  }
37
51
 
38
52
  if (operation.type === 'insert_node') {
39
- // Must be given a new key or adding/removing marks while typing gets in trouble (duped keys)!
40
- const withNewKey = !isPreservingKeys(editor) || !('_key' in operation.node)
41
-
42
53
  if (!Editor.isEditor(operation.node)) {
43
54
  apply({
44
55
  ...operation,
45
56
  node: {
46
57
  ...operation.node,
47
- ...(withNewKey ? {_key: keyGenerator()} : {}),
58
+ _key: keyGenerator(),
48
59
  },
49
60
  })
50
61
 
@@ -27,7 +27,6 @@ import {fromSlateValue, isEqualToEmptyEditor} from '../../utils/values'
27
27
  import {IS_PROCESSING_REMOTE_CHANGES, KEY_TO_VALUE_ELEMENT} from '../../utils/weakMaps'
28
28
  import {withRemoteChanges} from '../../utils/withChanges'
29
29
  import {isPatching, PATCHING, withoutPatching} from '../../utils/withoutPatching'
30
- import {withPreserveKeys} from '../../utils/withPreserveKeys'
31
30
  import {withoutSaving} from './createWithUndoRedo'
32
31
 
33
32
  const debug = debugWithName('plugin:withPatches')
@@ -117,11 +116,9 @@ export function createWithPatches({
117
116
  Editor.withoutNormalizing(editor, () => {
118
117
  withoutPatching(editor, () => {
119
118
  withoutSaving(editor, () => {
120
- withPreserveKeys(editor, () => {
121
- patches.forEach((patch) => {
122
- if (debug.enabled) debug(`Handling remote patch ${JSON.stringify(patch)}`)
123
- changed = applyPatch(editor, patch)
124
- })
119
+ patches.forEach((patch) => {
120
+ if (debug.enabled) debug(`Handling remote patch ${JSON.stringify(patch)}`)
121
+ changed = applyPatch(editor, patch)
125
122
  })
126
123
  })
127
124
  })
@@ -3,6 +3,8 @@ import {Editor, Path} from 'slate'
3
3
  import {type PortableTextSlateEditor} from '../../types/editor'
4
4
  import {type SlateTextBlock, type VoidElement} from '../../types/slate'
5
5
  import {debugWithName} from '../../utils/debug'
6
+ import {isChangingRemotely} from '../../utils/withChanges'
7
+ import {isRedoing, isUndoing} from '../../utils/withUndoRedo'
6
8
 
7
9
  const debug = debugWithName('plugin:withPlaceholderBlock')
8
10
 
@@ -17,6 +19,24 @@ export function createWithPlaceholderBlock(): (
17
19
  const {apply} = editor
18
20
 
19
21
  editor.apply = (op) => {
22
+ /**
23
+ * We don't want to run any side effects when the editor is processing
24
+ * remote changes.
25
+ */
26
+ if (isChangingRemotely(editor)) {
27
+ apply(op)
28
+ return
29
+ }
30
+
31
+ /**
32
+ * We don't want to run any side effects when the editor is undoing or
33
+ * redoing operations.
34
+ */
35
+ if (isUndoing(editor) || isRedoing(editor)) {
36
+ apply(op)
37
+ return
38
+ }
39
+
20
40
  if (op.type === 'remove_node') {
21
41
  const node = op.node as SlateTextBlock | VoidElement
22
42
  if (op.path[0] === 0 && Editor.isVoid(editor, node)) {
@@ -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,
@@ -19,7 +19,8 @@ import {
19
19
  import {debugWithName} from '../../utils/debug'
20
20
  import {toPortableTextRange} from '../../utils/ranges'
21
21
  import {EMPTY_MARKS} from '../../utils/values'
22
- import {withoutPreserveKeys} from '../../utils/withPreserveKeys'
22
+ import {isChangingRemotely} from '../../utils/withChanges'
23
+ import {isRedoing, isUndoing} from '../../utils/withUndoRedo'
23
24
 
24
25
  const debug = debugWithName('plugin:withPortableTextMarkModel')
25
26
 
@@ -53,29 +54,38 @@ export function createWithPortableTextMarkModel(
53
54
 
54
55
  // Extend Slate's default normalization. Merge spans with same set of .marks when doing merge_node operations, and clean up markDefs / marks
55
56
  editor.normalizeNode = (nodeEntry) => {
56
- normalizeNode(nodeEntry)
57
- if (
58
- editor.operations.some((op) =>
59
- [
60
- 'insert_node',
61
- 'insert_text',
62
- 'merge_node',
63
- 'remove_node',
64
- 'remove_text',
65
- 'set_node',
66
- ].includes(op.type),
67
- )
68
- ) {
69
- mergeSpans(editor)
70
- }
71
57
  const [node, path] = nodeEntry
58
+
72
59
  const isSpan = Text.isText(node) && node._type === types.span.name
73
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
+
74
84
  if (isSpan || isTextBlock) {
75
85
  if (isSpan && !Array.isArray(node.marks)) {
76
86
  debug('Adding .marks to span node')
77
87
  Transforms.setNodes(editor, {marks: []}, {at: path})
78
- editor.onChange()
88
+ return
79
89
  }
80
90
  const hasSpanMarks = isSpan && (node.marks || []).length > 0
81
91
  if (hasSpanMarks) {
@@ -99,7 +109,7 @@ export function createWithPortableTextMarkModel(
99
109
  {marks: spanMarks.filter((mark) => !orphanedMarks.includes(mark))},
100
110
  {at: path},
101
111
  )
102
- editor.onChange()
112
+ return
103
113
  }
104
114
  }
105
115
  }
@@ -123,7 +133,7 @@ export function createWithPortableTextMarkModel(
123
133
  // eslint-disable-next-line max-depth
124
134
  if (!isNormalized) {
125
135
  Transforms.setNodes(editor, {markDefs: newMarkDefs}, {at: targetPath, voids: false})
126
- editor.onChange()
136
+ return
127
137
  }
128
138
  }
129
139
  }
@@ -147,7 +157,7 @@ export function createWithPortableTextMarkModel(
147
157
  {markDefs: uniq([...oldDefs, ...op.properties.markDefs])},
148
158
  {at: targetPath, voids: false},
149
159
  )
150
- editor.onChange()
160
+ return
151
161
  }
152
162
  }
153
163
  // Make sure marks are reset, if a block is split at the end.
@@ -168,7 +178,7 @@ export function createWithPortableTextMarkModel(
168
178
  child.marks.length > 0
169
179
  ) {
170
180
  Transforms.setNodes(editor, {marks: []}, {at: childPath, voids: false})
171
- editor.onChange()
181
+ return
172
182
  }
173
183
  }
174
184
  // Make sure markDefs are reset, if a block is split at start.
@@ -191,7 +201,7 @@ export function createWithPortableTextMarkModel(
191
201
  (!block.children[0].marks || block.children[0].marks.length === 0)
192
202
  ) {
193
203
  Transforms.setNodes(editor, {markDefs: []}, {at: blockPath})
194
- editor.onChange()
204
+ return
195
205
  }
196
206
  }
197
207
  }
@@ -202,7 +212,7 @@ export function createWithPortableTextMarkModel(
202
212
  (!node.marks || (node.marks.length > 0 && node.text === ''))
203
213
  ) {
204
214
  Transforms.setNodes(editor, {marks: []}, {at: path, voids: false})
205
- editor.onChange()
215
+ return
206
216
  }
207
217
  }
208
218
  // Check consistency of markDefs (unless we are merging two nodes)
@@ -228,12 +238,32 @@ export function createWithPortableTextMarkModel(
228
238
  },
229
239
  {at: path},
230
240
  )
231
- editor.onChange()
241
+ return
232
242
  }
233
243
  }
244
+
245
+ normalizeNode(nodeEntry)
234
246
  }
235
247
 
236
248
  editor.apply = (op) => {
249
+ /**
250
+ * We don't want to run any side effects when the editor is processing
251
+ * remote changes.
252
+ */
253
+ if (isChangingRemotely(editor)) {
254
+ apply(op)
255
+ return
256
+ }
257
+
258
+ /**
259
+ * We don't want to run any side effects when the editor is undoing or
260
+ * redoing operations.
261
+ */
262
+ if (isUndoing(editor) || isRedoing(editor)) {
263
+ apply(op)
264
+ return
265
+ }
266
+
237
267
  // Special hook before inserting text at the end of an annotation.
238
268
  if (op.type === 'insert_text') {
239
269
  const {selection} = editor
@@ -295,21 +325,10 @@ export function createWithPortableTextMarkModel(
295
325
  const deletingFromTheEnd = op.offset + op.text.length === node.text.length
296
326
 
297
327
  if (nodeHasAnnotations && deletingPartOfTheNode && deletingFromTheEnd) {
298
- /**
299
- * If all of these conditions match then override the ordinary
300
- * `remove_text` operation and turn it into `split_nodes` followed
301
- * by `remove_nodes`. This is so if the operation can be properly
302
- * undone. Undoing a `remove_text` results in an `insert_text` and
303
- * we want to bail out of that in this exact scenario to make sure
304
- * the inserted text is annotated. (See custom logic regarding
305
- * `insert_text`)
306
- */
307
328
  Editor.withoutNormalizing(editor, () => {
308
- withoutPreserveKeys(editor, () => {
309
- Transforms.splitNodes(editor, {
310
- match: Text.isText,
311
- at: {path: op.path, offset: op.offset},
312
- })
329
+ Transforms.splitNodes(editor, {
330
+ match: Text.isText,
331
+ at: {path: op.path, offset: op.offset},
313
332
  })
314
333
  Transforms.removeNodes(editor, {at: Path.next(op.path)})
315
334
  })
@@ -317,6 +336,24 @@ export function createWithPortableTextMarkModel(
317
336
  editor.onChange()
318
337
  return
319
338
  }
339
+
340
+ const deletingAllText = op.offset === 0 && deletingFromTheEnd
341
+
342
+ if (nodeHasAnnotations && deletingAllText) {
343
+ const marksWithoutAnnotationMarks: string[] = (
344
+ {
345
+ ...(Editor.marks(editor) || {}),
346
+ }.marks || []
347
+ ).filter((mark) => decorators.includes(mark))
348
+
349
+ Editor.withoutNormalizing(editor, () => {
350
+ apply(op)
351
+ Transforms.setNodes(editor, {marks: marksWithoutAnnotationMarks}, {at: op.path})
352
+ })
353
+
354
+ editor.onChange()
355
+ return
356
+ }
320
357
  }
321
358
  }
322
359
 
@@ -327,34 +364,35 @@ export function createWithPortableTextMarkModel(
327
364
  editor.addMark = (mark: string) => {
328
365
  if (editor.selection) {
329
366
  if (Range.isExpanded(editor.selection)) {
330
- // Split if needed
331
- Transforms.setNodes(editor, {}, {match: Text.isText, split: true})
332
- // Use new selection
333
- const splitTextNodes = [
334
- ...Editor.nodes(editor, {at: editor.selection, match: Text.isText}),
335
- ]
336
- const shouldRemoveMark = splitTextNodes.every((node) => node[0].marks?.includes(mark))
337
-
338
- if (shouldRemoveMark) {
339
- editor.removeMark(mark)
340
- return editor
341
- }
342
367
  Editor.withoutNormalizing(editor, () => {
343
- splitTextNodes.forEach(([node, path]) => {
344
- const marks = [
345
- ...(Array.isArray(node.marks) ? node.marks : []).filter(
346
- (eMark: string) => eMark !== mark,
347
- ),
348
- mark,
349
- ]
350
- Transforms.setNodes(
351
- editor,
352
- {marks},
353
- {at: path, match: Text.isText, split: true, hanging: true},
354
- )
355
- })
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
+ }
356
395
  })
357
- Editor.normalize(editor)
358
396
  } else {
359
397
  const existingMarks: string[] =
360
398
  {
@@ -460,36 +498,4 @@ export function createWithPortableTextMarkModel(
460
498
  }
461
499
  return editor
462
500
  }
463
-
464
- /**
465
- * Normalize re-marked spans in selection
466
- */
467
- function mergeSpans(editor: PortableTextSlateEditor) {
468
- const {selection} = editor
469
-
470
- if (selection) {
471
- const textNodesInSelection = Array.from(
472
- Editor.nodes(editor, {
473
- at: Editor.range(editor, [selection.anchor.path[0]], [selection.focus.path[0]]),
474
- match: Text.isText,
475
- reverse: true,
476
- }),
477
- )
478
-
479
- for (const [node, path] of textNodesInSelection) {
480
- const [parent] = path.length > 1 ? Editor.node(editor, Path.parent(path)) : [undefined]
481
- const nextPath = [path[0], path[1] + 1]
482
-
483
- if (editor.isTextBlock(parent)) {
484
- const nextNode = parent.children[nextPath[1]]
485
-
486
- if (Text.isText(nextNode) && isEqual(nextNode.marks, node.marks)) {
487
- debug('Merging spans')
488
- Transforms.mergeNodes(editor, {at: nextPath, voids: true})
489
- editor.onChange()
490
- }
491
- }
492
- }
493
- }
494
- }
495
501
  }
@@ -12,7 +12,7 @@ import {type Descendant, Editor, Operation, Path, type SelectionOperation, Trans
12
12
  import {type PatchObservable, type PortableTextSlateEditor} from '../../types/editor'
13
13
  import {debugWithName} from '../../utils/debug'
14
14
  import {fromSlateValue} from '../../utils/values'
15
- import {withPreserveKeys} from '../../utils/withPreserveKeys'
15
+ import {setIsRedoing, setIsUndoing, withRedoing, withUndoing} from '../../utils/withUndoRedo'
16
16
 
17
17
  const debug = debugWithName('plugin:withUndoRedo')
18
18
  const debugVerbose = debug.enabled && false
@@ -150,7 +150,7 @@ export function createWithUndoRedo(
150
150
 
151
151
  try {
152
152
  Editor.withoutNormalizing(editor, () => {
153
- withPreserveKeys(editor, () => {
153
+ withUndoing(editor, () => {
154
154
  withoutSaving(editor, () => {
155
155
  reversedOperations.forEach((op) => {
156
156
  editor.apply(op)
@@ -166,6 +166,7 @@ export function createWithUndoRedo(
166
166
  Transforms.deselect(editor)
167
167
  editor.history = {undos: [], redos: []}
168
168
  SAVING.set(editor, true)
169
+ setIsUndoing(editor, false)
169
170
  editor.onChange()
170
171
  return
171
172
  }
@@ -195,7 +196,7 @@ export function createWithUndoRedo(
195
196
  })
196
197
  try {
197
198
  Editor.withoutNormalizing(editor, () => {
198
- withPreserveKeys(editor, () => {
199
+ withRedoing(editor, () => {
199
200
  withoutSaving(editor, () => {
200
201
  // eslint-disable-next-line max-nested-callbacks
201
202
  transformedOperations.forEach((op) => {
@@ -212,6 +213,7 @@ export function createWithUndoRedo(
212
213
  Transforms.deselect(editor)
213
214
  editor.history = {undos: [], redos: []}
214
215
  SAVING.set(editor, true)
216
+ setIsRedoing(editor, false)
215
217
  editor.onChange()
216
218
  return
217
219
  }
@@ -0,0 +1,34 @@
1
+ import {type Editor} from 'slate'
2
+
3
+ const IS_UDOING: WeakMap<Editor, boolean | undefined> = new WeakMap()
4
+ const IS_REDOING: WeakMap<Editor, boolean | undefined> = new WeakMap()
5
+
6
+ export function withUndoing(editor: Editor, fn: () => void) {
7
+ const prev = isUndoing(editor)
8
+ IS_UDOING.set(editor, true)
9
+ fn()
10
+ IS_UDOING.set(editor, prev)
11
+ }
12
+
13
+ export function isUndoing(editor: Editor) {
14
+ return IS_UDOING.get(editor) ?? false
15
+ }
16
+
17
+ export function setIsUndoing(editor: Editor, isUndoing: boolean) {
18
+ IS_UDOING.set(editor, isUndoing)
19
+ }
20
+
21
+ export function withRedoing(editor: Editor, fn: () => void) {
22
+ const prev = isRedoing(editor)
23
+ IS_REDOING.set(editor, true)
24
+ fn()
25
+ IS_REDOING.set(editor, prev)
26
+ }
27
+
28
+ export function isRedoing(editor: Editor) {
29
+ return IS_REDOING.get(editor) ?? false
30
+ }
31
+
32
+ export function setIsRedoing(editor: Editor, isRedoing: boolean) {
33
+ IS_REDOING.set(editor, isRedoing)
34
+ }