@portabletext/editor 1.0.19 → 1.1.1

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 (76) hide show
  1. package/lib/index.d.mts +142 -67
  2. package/lib/index.d.ts +142 -67
  3. package/lib/index.esm.js +1130 -371
  4. package/lib/index.esm.js.map +1 -1
  5. package/lib/index.js +1130 -371
  6. package/lib/index.js.map +1 -1
  7. package/lib/index.mjs +1130 -371
  8. package/lib/index.mjs.map +1 -1
  9. package/package.json +4 -18
  10. package/src/editor/Editable.tsx +128 -55
  11. package/src/editor/PortableTextEditor.tsx +66 -32
  12. package/src/editor/__tests__/PortableTextEditor.test.tsx +44 -18
  13. package/src/editor/__tests__/PortableTextEditorTester.tsx +50 -38
  14. package/src/editor/__tests__/RangeDecorations.test.tsx +4 -6
  15. package/src/editor/__tests__/handleClick.test.tsx +28 -9
  16. package/src/editor/__tests__/insert-block.test.tsx +24 -8
  17. package/src/editor/__tests__/pteWarningsSelfSolving.test.tsx +31 -63
  18. package/src/editor/__tests__/utils.ts +10 -4
  19. package/src/editor/components/DraggableBlock.tsx +36 -13
  20. package/src/editor/components/Element.tsx +73 -33
  21. package/src/editor/components/Leaf.tsx +114 -76
  22. package/src/editor/components/SlateContainer.tsx +14 -7
  23. package/src/editor/components/Synchronizer.tsx +8 -5
  24. package/src/editor/hooks/usePortableTextEditor.ts +3 -3
  25. package/src/editor/hooks/usePortableTextEditorSelection.tsx +10 -4
  26. package/src/editor/hooks/useSyncValue.test.tsx +9 -4
  27. package/src/editor/hooks/useSyncValue.ts +198 -133
  28. package/src/editor/nodes/DefaultAnnotation.tsx +6 -4
  29. package/src/editor/nodes/DefaultObject.tsx +1 -1
  30. package/src/editor/plugins/__tests__/createWithInsertData.test.tsx +23 -8
  31. package/src/editor/plugins/__tests__/withEditableAPIDelete.test.tsx +26 -9
  32. package/src/editor/plugins/__tests__/withEditableAPIGetFragment.test.tsx +15 -5
  33. package/src/editor/plugins/__tests__/withEditableAPIInsert.test.tsx +60 -19
  34. package/src/editor/plugins/__tests__/withEditableAPISelectionsOverlapping.test.tsx +5 -3
  35. package/src/editor/plugins/__tests__/withPortableTextLists.test.tsx +4 -2
  36. package/src/editor/plugins/__tests__/withPortableTextMarkModel.test.tsx +61 -19
  37. package/src/editor/plugins/__tests__/withPortableTextSelections.test.tsx +6 -3
  38. package/src/editor/plugins/__tests__/withUndoRedo.test.tsx +30 -13
  39. package/src/editor/plugins/createWithEditableAPI.ts +361 -131
  40. package/src/editor/plugins/createWithHotKeys.ts +46 -130
  41. package/src/editor/plugins/createWithInsertBreak.ts +167 -28
  42. package/src/editor/plugins/createWithInsertData.ts +66 -30
  43. package/src/editor/plugins/createWithMaxBlocks.ts +6 -3
  44. package/src/editor/plugins/createWithObjectKeys.ts +7 -3
  45. package/src/editor/plugins/createWithPatches.ts +66 -24
  46. package/src/editor/plugins/createWithPlaceholderBlock.ts +9 -5
  47. package/src/editor/plugins/createWithPortableTextBlockStyle.ts +17 -7
  48. package/src/editor/plugins/createWithPortableTextLists.ts +21 -9
  49. package/src/editor/plugins/createWithPortableTextMarkModel.ts +217 -52
  50. package/src/editor/plugins/createWithPortableTextSelections.ts +11 -9
  51. package/src/editor/plugins/createWithSchemaTypes.ts +26 -10
  52. package/src/editor/plugins/createWithUndoRedo.ts +106 -27
  53. package/src/editor/plugins/createWithUtils.ts +33 -11
  54. package/src/editor/plugins/index.ts +34 -13
  55. package/src/types/editor.ts +73 -44
  56. package/src/types/options.ts +7 -5
  57. package/src/types/slate.ts +6 -6
  58. package/src/utils/__tests__/dmpToOperations.test.ts +41 -16
  59. package/src/utils/__tests__/operationToPatches.test.ts +4 -3
  60. package/src/utils/__tests__/patchToOperations.test.ts +16 -5
  61. package/src/utils/__tests__/ranges.test.ts +9 -4
  62. package/src/utils/__tests__/valueNormalization.test.tsx +12 -4
  63. package/src/utils/__tests__/values.test.ts +0 -1
  64. package/src/utils/applyPatch.ts +78 -29
  65. package/src/utils/getPortableTextMemberSchemaTypes.ts +38 -23
  66. package/src/utils/operationToPatches.ts +123 -44
  67. package/src/utils/paths.ts +26 -9
  68. package/src/utils/ranges.ts +16 -10
  69. package/src/utils/selection.ts +21 -9
  70. package/src/utils/ucs2Indices.ts +2 -2
  71. package/src/utils/validateValue.ts +118 -45
  72. package/src/utils/values.ts +38 -17
  73. package/src/utils/weakMaps.ts +20 -10
  74. package/src/utils/withChanges.ts +5 -3
  75. package/src/utils/withUndoRedo.ts +1 -1
  76. package/src/utils/withoutPatching.ts +1 -1
@@ -1,16 +1,16 @@
1
- /* eslint-disable max-statements */
2
- /* eslint-disable complexity */
3
1
  import {isPortableTextSpan, isPortableTextTextBlock} from '@sanity/types'
4
2
  import {isHotkey} from 'is-hotkey-esm'
5
- import {type KeyboardEvent} from 'react'
3
+ import type {KeyboardEvent} from 'react'
6
4
  import {Editor, Node, Path, Range, Transforms} from 'slate'
7
- import {type ReactEditor} from 'slate-react'
8
-
9
- import {type PortableTextMemberSchemaTypes, type PortableTextSlateEditor} from '../../types/editor'
10
- import {type HotkeyOptions} from '../../types/options'
11
- import {type SlateTextBlock, type VoidElement} from '../../types/slate'
5
+ import type {ReactEditor} from 'slate-react'
6
+ import type {
7
+ PortableTextMemberSchemaTypes,
8
+ PortableTextSlateEditor,
9
+ } from '../../types/editor'
10
+ import type {HotkeyOptions} from '../../types/options'
11
+ import type {SlateTextBlock, VoidElement} from '../../types/slate'
12
12
  import {debugWithName} from '../../utils/debug'
13
- import {type PortableTextEditor} from '../PortableTextEditor'
13
+ import type {PortableTextEditor} from '../PortableTextEditor'
14
14
 
15
15
  const debug = debugWithName('plugin:withHotKeys')
16
16
 
@@ -40,7 +40,6 @@ export function createWithHotkeys(
40
40
  // Wire up custom marks hotkeys
41
41
  Object.keys(activeHotkeys).forEach((cat) => {
42
42
  if (cat === 'marks') {
43
- // eslint-disable-next-line guard-for-in
44
43
  for (const hotkey in activeHotkeys[cat]) {
45
44
  if (reservedHotkeys.includes(hotkey)) {
46
45
  throw new Error(`The hotkey ${hotkey} is reserved!`)
@@ -57,7 +56,6 @@ export function createWithHotkeys(
57
56
  }
58
57
  }
59
58
  if (cat === 'custom') {
60
- // eslint-disable-next-line guard-for-in
61
59
  for (const hotkey in activeHotkeys[cat]) {
62
60
  if (reservedHotkeys.includes(hotkey)) {
63
61
  throw new Error(`The hotkey ${hotkey} is reserved!`)
@@ -77,22 +75,27 @@ export function createWithHotkeys(
77
75
  const isTab = isHotkey('tab', event.nativeEvent)
78
76
  const isShiftEnter = isHotkey('shift+enter', event.nativeEvent)
79
77
  const isShiftTab = isHotkey('shift+tab', event.nativeEvent)
80
- const isBackspace = isHotkey('backspace', event.nativeEvent)
81
- const isDelete = isHotkey('delete', event.nativeEvent)
82
78
  const isArrowDown = isHotkey('down', event.nativeEvent)
83
79
  const isArrowUp = isHotkey('up', event.nativeEvent)
84
80
 
85
81
  // Check if the user is in a void block, in that case, add an empty text block below if there is no next block
86
82
  if (isArrowDown && editor.selection) {
87
- const focusBlock = Node.descendant(editor, editor.selection.focus.path.slice(0, 1)) as
88
- | SlateTextBlock
89
- | VoidElement
83
+ const focusBlock = Node.descendant(
84
+ editor,
85
+ editor.selection.focus.path.slice(0, 1),
86
+ ) as SlateTextBlock | VoidElement
90
87
 
91
88
  if (focusBlock && Editor.isVoid(editor, focusBlock)) {
92
89
  const nextPath = Path.next(editor.selection.focus.path.slice(0, 1))
93
90
  const nextBlock = Node.has(editor, nextPath)
94
91
  if (!nextBlock) {
95
- Transforms.insertNodes(editor, editor.pteCreateEmptyBlock(), {at: nextPath})
92
+ Transforms.insertNodes(
93
+ editor,
94
+ editor.pteCreateTextBlock({decorators: []}),
95
+ {
96
+ at: nextPath,
97
+ },
98
+ )
96
99
  editor.onChange()
97
100
  return
98
101
  }
@@ -100,117 +103,20 @@ export function createWithHotkeys(
100
103
  }
101
104
  if (isArrowUp && editor.selection) {
102
105
  const isFirstBlock = editor.selection.focus.path[0] === 0
103
- const focusBlock = Node.descendant(editor, editor.selection.focus.path.slice(0, 1)) as
104
- | SlateTextBlock
105
- | VoidElement
106
-
107
- if (isFirstBlock && focusBlock && Editor.isVoid(editor, focusBlock)) {
108
- Transforms.insertNodes(editor, editor.pteCreateEmptyBlock(), {at: [0]})
109
- Transforms.select(editor, {path: [0, 0], offset: 0})
110
- editor.onChange()
111
- return
112
- }
113
- }
114
- if (
115
- isBackspace &&
116
- editor.selection &&
117
- editor.selection.focus.path[0] === 0 &&
118
- Range.isCollapsed(editor.selection)
119
- ) {
120
- // If the block is text and we have a next block below, remove the current block
121
- const focusBlock = Node.descendant(editor, editor.selection.focus.path.slice(0, 1)) as
122
- | SlateTextBlock
123
- | VoidElement
124
- const nextPath = Path.next(editor.selection.focus.path.slice(0, 1))
125
- const nextBlock = Node.has(editor, nextPath)
126
- const isTextBlock = isPortableTextTextBlock(focusBlock)
127
- const isEmptyFocusBlock =
128
- isTextBlock && focusBlock.children.length === 1 && focusBlock.children?.[0]?.text === ''
129
-
130
- if (nextBlock && isTextBlock && isEmptyFocusBlock) {
131
- // Remove current block
132
- event.preventDefault()
133
- event.stopPropagation()
134
- Transforms.removeNodes(editor, {match: (n) => n === focusBlock})
135
- editor.onChange()
136
- return
137
- }
138
- }
139
- // Disallow deleting void blocks by backspace from another line.
140
- // Otherwise it's so easy to delete the void block above when trying to delete text on
141
- // the line below or above
142
- if (
143
- isBackspace &&
144
- editor.selection &&
145
- editor.selection.focus.path[0] > 0 &&
146
- Range.isCollapsed(editor.selection)
147
- ) {
148
- const prevPath = Path.previous(editor.selection.focus.path.slice(0, 1))
149
- const prevBlock = Node.descendant(editor, prevPath) as SlateTextBlock | VoidElement
150
- const focusBlock = Node.descendant(editor, editor.selection.focus.path.slice(0, 1))
151
- if (
152
- prevBlock &&
153
- focusBlock &&
154
- Editor.isVoid(editor, prevBlock) &&
155
- editor.selection.focus.offset === 0
156
- ) {
157
- debug('Preventing deleting void block above')
158
- event.preventDefault()
159
- event.stopPropagation()
160
-
161
- const isTextBlock = isPortableTextTextBlock(focusBlock)
162
- const isEmptyFocusBlock =
163
- isTextBlock && focusBlock.children.length === 1 && focusBlock.children?.[0]?.text === ''
164
-
165
- // If this is a not an text block or it is empty, simply remove it
166
- if (!isTextBlock || isEmptyFocusBlock) {
167
- Transforms.removeNodes(editor, {match: (n) => n === focusBlock})
168
- Transforms.select(editor, prevPath)
169
-
170
- editor.onChange()
171
- return
172
- }
173
-
174
- // If the focused block is a text node but it isn't empty, focus on the previous block
175
- if (isTextBlock && !isEmptyFocusBlock) {
176
- Transforms.select(editor, prevPath)
177
-
178
- editor.onChange()
179
- return
180
- }
181
-
182
- return
183
- }
184
- }
185
- if (
186
- isDelete &&
187
- editor.selection &&
188
- editor.selection.focus.offset === 0 &&
189
- Range.isCollapsed(editor.selection) &&
190
- editor.children[editor.selection.focus.path[0] + 1]
191
- ) {
192
- const nextBlock = Node.descendant(
106
+ const focusBlock = Node.descendant(
193
107
  editor,
194
- Path.next(editor.selection.focus.path.slice(0, 1)),
108
+ editor.selection.focus.path.slice(0, 1),
195
109
  ) as SlateTextBlock | VoidElement
196
- const focusBlockPath = editor.selection.focus.path.slice(0, 1)
197
- const focusBlock = Node.descendant(editor, focusBlockPath) as SlateTextBlock | VoidElement
198
- const isTextBlock = isPortableTextTextBlock(focusBlock)
199
- const isEmptyFocusBlock =
200
- isTextBlock && focusBlock.children.length === 1 && focusBlock.children?.[0]?.text === ''
201
110
 
202
- if (
203
- nextBlock &&
204
- focusBlock &&
205
- !Editor.isVoid(editor, focusBlock) &&
206
- Editor.isVoid(editor, nextBlock) &&
207
- isEmptyFocusBlock
208
- ) {
209
- debug('Preventing deleting void block below')
210
- event.preventDefault()
211
- event.stopPropagation()
212
- Transforms.removeNodes(editor, {match: (n) => n === focusBlock})
213
- Transforms.select(editor, focusBlockPath)
111
+ if (isFirstBlock && focusBlock && Editor.isVoid(editor, focusBlock)) {
112
+ Transforms.insertNodes(
113
+ editor,
114
+ editor.pteCreateTextBlock({decorators: []}),
115
+ {
116
+ at: [0],
117
+ },
118
+ )
119
+ Transforms.select(editor, {path: [0, 0], offset: 0})
214
120
  editor.onChange()
215
121
  return
216
122
  }
@@ -220,7 +126,9 @@ export function createWithHotkeys(
220
126
  // Only steal tab when we are on a plain text span or we are at the start of the line (fallback if the whole block is annotated or contains a single inline object)
221
127
  // Otherwise tab is reserved for accessability for buttons etc.
222
128
  if ((isTab || isShiftTab) && editor.selection) {
223
- const [focusChild] = Editor.node(editor, editor.selection.focus, {depth: 2})
129
+ const [focusChild] = Editor.node(editor, editor.selection.focus, {
130
+ depth: 2,
131
+ })
224
132
  const [focusBlock] = isPortableTextSpan(focusChild)
225
133
  ? Editor.node(editor, editor.selection.focus, {depth: 1})
226
134
  : []
@@ -247,7 +155,9 @@ export function createWithHotkeys(
247
155
  // Deal with enter key combos
248
156
  if (isEnter && !isShiftEnter && editor.selection) {
249
157
  const focusBlockPath = editor.selection.focus.path.slice(0, 1)
250
- const focusBlock = Node.descendant(editor, focusBlockPath) as SlateTextBlock | VoidElement
158
+ const focusBlock = Node.descendant(editor, focusBlockPath) as
159
+ | SlateTextBlock
160
+ | VoidElement
251
161
 
252
162
  // List item enter key
253
163
  if (editor.isListBlock(focusBlock)) {
@@ -266,7 +176,10 @@ export function createWithHotkeys(
266
176
  const [, end] = Range.edges(editor.selection)
267
177
  const endAtEndOfNode = Editor.isEnd(editor, end, end.path)
268
178
  if (endAtEndOfNode) {
269
- Editor.insertNode(editor, editor.pteCreateEmptyBlock())
179
+ Editor.insertNode(
180
+ editor,
181
+ editor.pteCreateTextBlock({decorators: []}),
182
+ )
270
183
  event.preventDefault()
271
184
  editor.onChange()
272
185
  return
@@ -274,7 +187,7 @@ export function createWithHotkeys(
274
187
  }
275
188
  // Block object enter key
276
189
  if (focusBlock && Editor.isVoid(editor, focusBlock)) {
277
- Editor.insertNode(editor, editor.pteCreateEmptyBlock())
190
+ Editor.insertNode(editor, editor.pteCreateTextBlock({decorators: []}))
278
191
  event.preventDefault()
279
192
  editor.onChange()
280
193
  return
@@ -298,7 +211,10 @@ export function createWithHotkeys(
298
211
  editor.undo()
299
212
  return
300
213
  }
301
- if (isHotkey('mod+y', event.nativeEvent) || isHotkey('mod+shift+z', event.nativeEvent)) {
214
+ if (
215
+ isHotkey('mod+y', event.nativeEvent) ||
216
+ isHotkey('mod+shift+z', event.nativeEvent)
217
+ ) {
302
218
  event.preventDefault()
303
219
  editor.redo()
304
220
  }
@@ -1,45 +1,184 @@
1
+ import {isEqual} from 'lodash'
1
2
  import {Editor, Node, Path, Range, Transforms} from 'slate'
3
+ import type {
4
+ PortableTextMemberSchemaTypes,
5
+ PortableTextSlateEditor,
6
+ } from '../../types/editor'
7
+ import type {SlateTextBlock, VoidElement} from '../../types/slate'
2
8
 
3
- import {type PortableTextMemberSchemaTypes, type PortableTextSlateEditor} from '../../types/editor'
4
- import {type SlateTextBlock, type VoidElement} from '../../types/slate'
5
- import {isEqualToEmptyEditor} from '../../utils/values'
6
-
7
- /**
8
- * Changes default behavior of insertBreak to insert a new block instead of splitting current when the cursor is at the
9
- * start of the block.
10
- */
11
9
  export function createWithInsertBreak(
12
10
  types: PortableTextMemberSchemaTypes,
11
+ keyGenerator: () => string,
13
12
  ): (editor: PortableTextSlateEditor) => PortableTextSlateEditor {
14
- return function withInsertBreak(editor: PortableTextSlateEditor): PortableTextSlateEditor {
13
+ return function withInsertBreak(
14
+ editor: PortableTextSlateEditor,
15
+ ): PortableTextSlateEditor {
15
16
  const {insertBreak} = editor
16
17
 
17
18
  editor.insertBreak = () => {
18
- if (editor.selection) {
19
- const focusBlockPath = editor.selection.focus.path.slice(0, 1)
20
- const focusBlock = Node.descendant(editor, focusBlockPath) as SlateTextBlock | VoidElement
21
-
22
- if (editor.isTextBlock(focusBlock)) {
23
- // Enter from another style than the first (default one)
24
- const [, end] = Range.edges(editor.selection)
25
- // If it's at the start of block, we want to preserve the current block key and insert a new one in the current position instead of splitting the node.
26
- const isEndAtStartOfNode = Editor.isStart(editor, end, end.path)
27
- const isEmptyTextBlock = focusBlock && isEqualToEmptyEditor([focusBlock], types)
28
- if (isEndAtStartOfNode && !isEmptyTextBlock) {
29
- Editor.insertNode(editor, editor.pteCreateEmptyBlock())
30
- const [nextBlockPath] = Path.next(focusBlockPath)
31
- Transforms.select(editor, {
32
- anchor: {path: [nextBlockPath, 0], offset: 0},
33
- focus: {path: [nextBlockPath, 0], offset: 0},
19
+ if (!editor.selection) {
20
+ insertBreak()
21
+ return
22
+ }
23
+
24
+ const focusBlockPath = editor.selection.focus.path.slice(0, 1)
25
+ const focusBlock = Node.descendant(editor, focusBlockPath) as
26
+ | SlateTextBlock
27
+ | VoidElement
28
+
29
+ if (editor.isTextBlock(focusBlock)) {
30
+ const [start, end] = Range.edges(editor.selection)
31
+ const isEndAtStartOfBlock = isEqual(end, {
32
+ path: [...focusBlockPath, 0],
33
+ offset: 0,
34
+ })
35
+
36
+ if (isEndAtStartOfBlock && Range.isCollapsed(editor.selection)) {
37
+ const focusDecorators = editor.isTextSpan(focusBlock.children[0])
38
+ ? (focusBlock.children[0].marks ?? []).filter((mark) =>
39
+ types.decorators.some((decorator) => decorator.value === mark),
40
+ )
41
+ : []
42
+
43
+ Editor.insertNode(
44
+ editor,
45
+ editor.pteCreateTextBlock({decorators: focusDecorators}),
46
+ )
47
+
48
+ const [nextBlockPath] = Path.next(focusBlockPath)
49
+
50
+ Transforms.select(editor, {
51
+ anchor: {path: [nextBlockPath, 0], offset: 0},
52
+ focus: {path: [nextBlockPath, 0], offset: 0},
53
+ })
54
+
55
+ editor.onChange()
56
+ return
57
+ }
58
+
59
+ const lastFocusBlockChild =
60
+ focusBlock.children[focusBlock.children.length - 1]
61
+ const isStartAtEndOfBlock = isEqual(start, {
62
+ path: [...focusBlockPath, focusBlock.children.length - 1],
63
+ offset: editor.isTextSpan(lastFocusBlockChild)
64
+ ? lastFocusBlockChild.text.length
65
+ : 0,
66
+ })
67
+ const isInTheMiddleOfNode = !isEndAtStartOfBlock && !isStartAtEndOfBlock
68
+
69
+ if (isInTheMiddleOfNode) {
70
+ Editor.withoutNormalizing(editor, () => {
71
+ if (!editor.selection) {
72
+ return
73
+ }
74
+
75
+ Transforms.splitNodes(editor, {
76
+ at: editor.selection,
34
77
  })
35
78
 
36
- editor.onChange()
37
- return
38
- }
79
+ const [nextNode, nextNodePath] = Editor.node(
80
+ editor,
81
+ Path.next(focusBlockPath),
82
+ {depth: 1},
83
+ )
84
+
85
+ Transforms.setSelection(editor, {
86
+ anchor: {path: [...nextNodePath, 0], offset: 0},
87
+ focus: {path: [...nextNodePath, 0], offset: 0},
88
+ })
89
+
90
+ /**
91
+ * Assign new keys to markDefs that are now split across two blocks
92
+ */
93
+ if (
94
+ editor.isTextBlock(nextNode) &&
95
+ nextNode.markDefs &&
96
+ nextNode.markDefs.length > 0
97
+ ) {
98
+ const newMarkDefKeys = new Map<string, string>()
99
+
100
+ const prevNodeSpans = Array.from(
101
+ Node.children(editor, focusBlockPath),
102
+ )
103
+ .map((entry) => entry[0])
104
+ .filter((node) => editor.isTextSpan(node))
105
+ const children = Node.children(editor, nextNodePath)
106
+
107
+ for (const [child, childPath] of children) {
108
+ if (!editor.isTextSpan(child)) {
109
+ continue
110
+ }
111
+
112
+ const marks = child.marks ?? []
113
+
114
+ // Go through the marks of the span and figure out if any of
115
+ // them refer to annotations that are also present in the
116
+ // previous block
117
+ for (const mark of marks) {
118
+ if (
119
+ types.decorators.some(
120
+ (decorator) => decorator.value === mark,
121
+ )
122
+ ) {
123
+ continue
124
+ }
125
+
126
+ if (
127
+ prevNodeSpans.some((prevNodeSpan) =>
128
+ prevNodeSpan.marks?.includes(mark),
129
+ ) &&
130
+ !newMarkDefKeys.has(mark)
131
+ ) {
132
+ // This annotation is both present in the previous block
133
+ // and this block, so let's assign a new key to it
134
+ newMarkDefKeys.set(mark, keyGenerator())
135
+ }
136
+ }
137
+
138
+ const newMarks = marks.map(
139
+ (mark) => newMarkDefKeys.get(mark) ?? mark,
140
+ )
141
+
142
+ // No need to update the marks if they are the same
143
+ if (!isEqual(marks, newMarks)) {
144
+ Transforms.setNodes(
145
+ editor,
146
+ {marks: newMarks},
147
+ {
148
+ at: childPath,
149
+ },
150
+ )
151
+ }
152
+ }
153
+
154
+ // Time to update all the markDefs that need a new key because
155
+ // they've been split across blocks
156
+ const newMarkDefs = nextNode.markDefs.map((markDef) => ({
157
+ ...markDef,
158
+ _key: newMarkDefKeys.get(markDef._key) ?? markDef._key,
159
+ }))
160
+
161
+ // No need to update the markDefs if they are the same
162
+ if (!isEqual(nextNode.markDefs, newMarkDefs)) {
163
+ Transforms.setNodes(
164
+ editor,
165
+ {markDefs: newMarkDefs},
166
+ {
167
+ at: nextNodePath,
168
+ match: (node) => editor.isTextBlock(node),
169
+ },
170
+ )
171
+ }
172
+ }
173
+ })
174
+ editor.onChange()
175
+ return
39
176
  }
40
177
  }
178
+
41
179
  insertBreak()
42
180
  }
181
+
43
182
  return editor
44
183
  }
45
184
  }
@@ -1,17 +1,20 @@
1
1
  import {htmlToBlocks, normalizeBlock} from '@sanity/block-tools'
2
- import {type PortableTextBlock, type PortableTextChild} from '@sanity/types'
2
+ import type {PortableTextBlock, PortableTextChild} from '@sanity/types'
3
3
  import {isEqual, uniq} from 'lodash'
4
- import {type Descendant, Editor, type Node, Range, Transforms} from 'slate'
4
+ import {Editor, Range, Transforms, type Descendant, type Node} from 'slate'
5
5
  import {ReactEditor} from 'slate-react'
6
-
7
- import {
8
- type EditorChanges,
9
- type PortableTextMemberSchemaTypes,
10
- type PortableTextSlateEditor,
6
+ import type {
7
+ EditorChanges,
8
+ PortableTextMemberSchemaTypes,
9
+ PortableTextSlateEditor,
11
10
  } from '../../types/editor'
12
11
  import {debugWithName} from '../../utils/debug'
13
12
  import {validateValue} from '../../utils/validateValue'
14
- import {fromSlateValue, isEqualToEmptyEditor, toSlateValue} from '../../utils/values'
13
+ import {
14
+ fromSlateValue,
15
+ isEqualToEmptyEditor,
16
+ toSlateValue,
17
+ } from '../../utils/values'
15
18
 
16
19
  const debug = debugWithName('plugin:withInsertData')
17
20
 
@@ -24,10 +27,13 @@ export function createWithInsertData(
24
27
  schemaTypes: PortableTextMemberSchemaTypes,
25
28
  keyGenerator: () => string,
26
29
  ) {
27
- return function withInsertData(editor: PortableTextSlateEditor): PortableTextSlateEditor {
30
+ return function withInsertData(
31
+ editor: PortableTextSlateEditor,
32
+ ): PortableTextSlateEditor {
28
33
  const blockTypeName = schemaTypes.block.name
29
34
  const spanTypeName = schemaTypes.span.name
30
- const whitespaceOnPasteMode = schemaTypes.block.options.unstable_whitespaceOnPasteMode
35
+ const whitespaceOnPasteMode =
36
+ schemaTypes.block.options.unstable_whitespaceOnPasteMode
31
37
 
32
38
  const toPlainText = (blocks: PortableTextBlock[]) => {
33
39
  return blocks
@@ -39,13 +45,15 @@ export function createWithInsertData(
39
45
  return child.text
40
46
  }
41
47
  return `[${
42
- schemaTypes.inlineObjects.find((t) => t.name === child._type)?.title || 'Object'
48
+ schemaTypes.inlineObjects.find((t) => t.name === child._type)
49
+ ?.title || 'Object'
43
50
  }]`
44
51
  })
45
52
  .join('')
46
53
  }
47
54
  return `[${
48
- schemaTypes.blockObjects.find((t) => t.name === block._type)?.title || 'Object'
55
+ schemaTypes.blockObjects.find((t) => t.name === block._type)
56
+ ?.title || 'Object'
49
57
  }]`
50
58
  })
51
59
  .join('\n\n')
@@ -82,10 +90,12 @@ export function createWithInsertData(
82
90
  }
83
91
  // Remove any zero-width space spans from the cloned DOM so that they don't
84
92
  // show up elsewhere when pasted.
85
- Array.from(contents.querySelectorAll('[data-slate-zero-width]')).forEach((zw) => {
86
- const isNewline = zw.getAttribute('data-slate-zero-width') === 'n'
87
- zw.textContent = isNewline ? '\n' : ''
88
- })
93
+ Array.from(contents.querySelectorAll('[data-slate-zero-width]')).forEach(
94
+ (zw) => {
95
+ const isNewline = zw.getAttribute('data-slate-zero-width') === 'n'
96
+ zw.textContent = isNewline ? '\n' : ''
97
+ },
98
+ )
89
99
  // Clean up the clipboard HTML for editor spesific attributes
90
100
  Array.from(contents.querySelectorAll('*')).forEach((elm) => {
91
101
  elm.removeAttribute('contentEditable')
@@ -119,7 +129,10 @@ export function createWithInsertData(
119
129
  data.setData('application/json', asJSON)
120
130
  data.setData('application/x-portable-text', asJSON)
121
131
  debug('text', asPlainText)
122
- data.setData('application/x-portable-text-event-origin', originEvent || 'external')
132
+ data.setData(
133
+ 'application/x-portable-text-event-origin',
134
+ originEvent || 'external',
135
+ )
123
136
  debug('Set fragment data', asJSON, asHTML)
124
137
  }
125
138
 
@@ -175,12 +188,14 @@ export function createWithInsertData(
175
188
  debug('Inserting data', data)
176
189
  let portableText: PortableTextBlock[]
177
190
  let fragment: Node[]
178
- let insertedType
191
+ let insertedType: string | undefined
179
192
 
180
193
  if (html) {
181
194
  portableText = htmlToBlocks(html, schemaTypes.portableText, {
182
195
  unstable_whitespaceOnPasteMode: whitespaceOnPasteMode,
183
- }).map((block) => normalizeBlock(block, {blockTypeName})) as PortableTextBlock[]
196
+ }).map((block) =>
197
+ normalizeBlock(block, {blockTypeName}),
198
+ ) as PortableTextBlock[]
184
199
  fragment = toSlateValue(portableText, {schemaTypes})
185
200
  insertedType = 'HTML'
186
201
 
@@ -192,12 +207,14 @@ export function createWithInsertData(
192
207
  const blocks = escapeHtml(text)
193
208
  .split(/\n{2,}/)
194
209
  .map((line) =>
195
- line ? `<p>${line.replace(/(?:\r\n|\r|\n)/g, '<br/>')}</p>` : '<p></p>',
210
+ line
211
+ ? `<p>${line.replace(/(?:\r\n|\r|\n)/g, '<br/>')}</p>`
212
+ : '<p></p>',
196
213
  )
197
214
  .join('')
198
215
  const textToHtml = `<html><body>${blocks}</body></html>`
199
- portableText = htmlToBlocks(textToHtml, schemaTypes.portableText).map((block) =>
200
- normalizeBlock(block, {blockTypeName}),
216
+ portableText = htmlToBlocks(textToHtml, schemaTypes.portableText).map(
217
+ (block) => normalizeBlock(block, {blockTypeName}),
201
218
  ) as PortableTextBlock[]
202
219
  fragment = toSlateValue(portableText, {
203
220
  schemaTypes,
@@ -206,7 +223,11 @@ export function createWithInsertData(
206
223
  }
207
224
 
208
225
  // Validate the result
209
- const validation = validateValue(portableText, schemaTypes, keyGenerator)
226
+ const validation = validateValue(
227
+ portableText,
228
+ schemaTypes,
229
+ keyGenerator,
230
+ )
210
231
 
211
232
  // Bail out if it's not valid
212
233
  if (!validation.valid) {
@@ -221,7 +242,9 @@ export function createWithInsertData(
221
242
  debug('Invalid insert result', validation)
222
243
  return false
223
244
  }
224
- debug(`Inserting ${insertedType} fragment at ${JSON.stringify(editor.selection)}`)
245
+ debug(
246
+ `Inserting ${insertedType} fragment at ${JSON.stringify(editor.selection)}`,
247
+ )
225
248
  _insertFragment(editor, fragment, schemaTypes)
226
249
  change$.next({type: 'loading', isLoading: false})
227
250
  return true
@@ -312,8 +335,9 @@ function _regenerateKeys(
312
335
  ...child,
313
336
  marks:
314
337
  child.marks && child.marks.includes(oldKey)
315
- ? // eslint-disable-next-line max-nested-callbacks
316
- [...child.marks].filter((mark) => mark !== oldKey).concat(newKey)
338
+ ? [...child.marks]
339
+ .filter((mark) => mark !== oldKey)
340
+ .concat(newKey)
317
341
  : child.marks,
318
342
  }
319
343
  : child,
@@ -347,22 +371,34 @@ function _insertFragment(
347
371
  return
348
372
  }
349
373
  // Ensure that markDefs for any annotations inside this fragment are copied over to the focused text block.
350
- const [focusBlock, focusPath] = Editor.node(editor, editor.selection, {depth: 1})
374
+ const [focusBlock, focusPath] = Editor.node(editor, editor.selection, {
375
+ depth: 1,
376
+ })
351
377
  if (editor.isTextBlock(focusBlock) && editor.isTextBlock(fragment[0])) {
352
378
  const {markDefs} = focusBlock
353
- debug('Mixing markDefs of focusBlock and fragments[0] block', markDefs, fragment[0].markDefs)
379
+ debug(
380
+ 'Mixing markDefs of focusBlock and fragments[0] block',
381
+ markDefs,
382
+ fragment[0].markDefs,
383
+ )
354
384
  if (!isEqual(markDefs, fragment[0].markDefs)) {
355
385
  Transforms.setNodes(
356
386
  editor,
357
387
  {
358
- markDefs: uniq([...(fragment[0].markDefs || []), ...(markDefs || [])]),
388
+ markDefs: uniq([
389
+ ...(fragment[0].markDefs || []),
390
+ ...(markDefs || []),
391
+ ]),
359
392
  },
360
393
  {at: focusPath, mode: 'lowest', voids: false},
361
394
  )
362
395
  }
363
396
  }
364
397
 
365
- const isPasteToEmptyEditor = isEqualToEmptyEditor(editor.children, schemaTypes)
398
+ const isPasteToEmptyEditor = isEqualToEmptyEditor(
399
+ editor.children,
400
+ schemaTypes,
401
+ )
366
402
 
367
403
  if (isPasteToEmptyEditor) {
368
404
  // Special case for pasting directly into an empty editor (a placeholder block).