@portabletext/editor 1.0.18 → 1.1.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 (75) hide show
  1. package/lib/index.d.mts +140 -66
  2. package/lib/index.d.ts +140 -66
  3. package/lib/index.esm.js +1164 -410
  4. package/lib/index.esm.js.map +1 -1
  5. package/lib/index.js +1164 -410
  6. package/lib/index.js.map +1 -1
  7. package/lib/index.mjs +1164 -410
  8. package/lib/index.mjs.map +1 -1
  9. package/package.json +8 -4
  10. package/src/editor/Editable.tsx +107 -36
  11. package/src/editor/PortableTextEditor.tsx +47 -12
  12. package/src/editor/__tests__/PortableTextEditor.test.tsx +42 -15
  13. package/src/editor/__tests__/PortableTextEditorTester.tsx +50 -38
  14. package/src/editor/__tests__/RangeDecorations.test.tsx +0 -1
  15. package/src/editor/__tests__/handleClick.test.tsx +28 -9
  16. package/src/editor/__tests__/insert-block.test.tsx +22 -6
  17. package/src/editor/__tests__/pteWarningsSelfSolving.test.tsx +30 -62
  18. package/src/editor/__tests__/utils.ts +10 -3
  19. package/src/editor/components/DraggableBlock.tsx +36 -13
  20. package/src/editor/components/Element.tsx +59 -17
  21. package/src/editor/components/Leaf.tsx +106 -68
  22. package/src/editor/components/SlateContainer.tsx +12 -5
  23. package/src/editor/components/Synchronizer.tsx +5 -2
  24. package/src/editor/hooks/usePortableTextEditor.ts +2 -2
  25. package/src/editor/hooks/usePortableTextEditorSelection.tsx +9 -3
  26. package/src/editor/hooks/useSyncValue.test.tsx +9 -4
  27. package/src/editor/hooks/useSyncValue.ts +199 -130
  28. package/src/editor/nodes/DefaultAnnotation.tsx +6 -3
  29. package/src/editor/plugins/__tests__/createWithInsertData.test.tsx +25 -7
  30. package/src/editor/plugins/__tests__/withEditableAPIDelete.test.tsx +26 -9
  31. package/src/editor/plugins/__tests__/withEditableAPIGetFragment.test.tsx +15 -5
  32. package/src/editor/plugins/__tests__/withEditableAPIInsert.test.tsx +60 -19
  33. package/src/editor/plugins/__tests__/withEditableAPISelectionsOverlapping.test.tsx +4 -2
  34. package/src/editor/plugins/__tests__/withPortableTextLists.test.tsx +4 -2
  35. package/src/editor/plugins/__tests__/withPortableTextMarkModel.test.tsx +61 -550
  36. package/src/editor/plugins/__tests__/withPortableTextSelections.test.tsx +6 -3
  37. package/src/editor/plugins/__tests__/withUndoRedo.test.tsx +30 -13
  38. package/src/editor/plugins/createWithEditableAPI.ts +354 -115
  39. package/src/editor/plugins/createWithHotKeys.ts +41 -121
  40. package/src/editor/plugins/createWithInsertBreak.ts +166 -27
  41. package/src/editor/plugins/createWithInsertData.ts +60 -23
  42. package/src/editor/plugins/createWithMaxBlocks.ts +5 -2
  43. package/src/editor/plugins/createWithObjectKeys.ts +7 -3
  44. package/src/editor/plugins/createWithPatches.ts +60 -16
  45. package/src/editor/plugins/createWithPlaceholderBlock.ts +7 -3
  46. package/src/editor/plugins/createWithPortableTextBlockStyle.ts +17 -7
  47. package/src/editor/plugins/createWithPortableTextLists.ts +21 -8
  48. package/src/editor/plugins/createWithPortableTextMarkModel.ts +301 -155
  49. package/src/editor/plugins/createWithPortableTextSelections.ts +4 -2
  50. package/src/editor/plugins/createWithSchemaTypes.ts +25 -9
  51. package/src/editor/plugins/createWithUndoRedo.ts +107 -24
  52. package/src/editor/plugins/createWithUtils.ts +32 -10
  53. package/src/editor/plugins/index.ts +31 -10
  54. package/src/types/editor.ts +44 -15
  55. package/src/types/options.ts +4 -2
  56. package/src/types/slate.ts +2 -2
  57. package/src/utils/__tests__/dmpToOperations.test.ts +38 -13
  58. package/src/utils/__tests__/operationToPatches.test.ts +3 -2
  59. package/src/utils/__tests__/patchToOperations.test.ts +15 -4
  60. package/src/utils/__tests__/ranges.test.ts +8 -3
  61. package/src/utils/__tests__/valueNormalization.test.tsx +12 -4
  62. package/src/utils/__tests__/values.test.ts +0 -1
  63. package/src/utils/applyPatch.ts +71 -20
  64. package/src/utils/getPortableTextMemberSchemaTypes.ts +30 -15
  65. package/src/utils/operationToPatches.ts +126 -43
  66. package/src/utils/paths.ts +24 -7
  67. package/src/utils/ranges.ts +12 -5
  68. package/src/utils/selection.ts +19 -7
  69. package/src/utils/validateValue.ts +118 -45
  70. package/src/utils/values.ts +31 -10
  71. package/src/utils/weakMaps.ts +18 -8
  72. package/src/utils/withChanges.ts +4 -2
  73. package/src/editor/plugins/__tests__/withHotkeys.test.tsx +0 -212
  74. package/src/editor/plugins/__tests__/withInsertBreak.test.tsx +0 -220
  75. package/src/editor/plugins/__tests__/withPlaceholderBlock.test.tsx +0 -133
@@ -5,8 +5,10 @@ import {isHotkey} from 'is-hotkey-esm'
5
5
  import {type KeyboardEvent} from 'react'
6
6
  import {Editor, Node, Path, Range, Transforms} from 'slate'
7
7
  import {type ReactEditor} from 'slate-react'
8
-
9
- import {type PortableTextMemberSchemaTypes, type PortableTextSlateEditor} from '../../types/editor'
8
+ import {
9
+ type PortableTextMemberSchemaTypes,
10
+ type PortableTextSlateEditor,
11
+ } from '../../types/editor'
10
12
  import {type HotkeyOptions} from '../../types/options'
11
13
  import {type SlateTextBlock, type VoidElement} from '../../types/slate'
12
14
  import {debugWithName} from '../../utils/debug'
@@ -77,22 +79,27 @@ export function createWithHotkeys(
77
79
  const isTab = isHotkey('tab', event.nativeEvent)
78
80
  const isShiftEnter = isHotkey('shift+enter', event.nativeEvent)
79
81
  const isShiftTab = isHotkey('shift+tab', event.nativeEvent)
80
- const isBackspace = isHotkey('backspace', event.nativeEvent)
81
- const isDelete = isHotkey('delete', event.nativeEvent)
82
82
  const isArrowDown = isHotkey('down', event.nativeEvent)
83
83
  const isArrowUp = isHotkey('up', event.nativeEvent)
84
84
 
85
85
  // 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
86
  if (isArrowDown && editor.selection) {
87
- const focusBlock = Node.descendant(editor, editor.selection.focus.path.slice(0, 1)) as
88
- | SlateTextBlock
89
- | VoidElement
87
+ const focusBlock = Node.descendant(
88
+ editor,
89
+ editor.selection.focus.path.slice(0, 1),
90
+ ) as SlateTextBlock | VoidElement
90
91
 
91
92
  if (focusBlock && Editor.isVoid(editor, focusBlock)) {
92
93
  const nextPath = Path.next(editor.selection.focus.path.slice(0, 1))
93
94
  const nextBlock = Node.has(editor, nextPath)
94
95
  if (!nextBlock) {
95
- Transforms.insertNodes(editor, editor.pteCreateEmptyBlock(), {at: nextPath})
96
+ Transforms.insertNodes(
97
+ editor,
98
+ editor.pteCreateTextBlock({decorators: []}),
99
+ {
100
+ at: nextPath,
101
+ },
102
+ )
96
103
  editor.onChange()
97
104
  return
98
105
  }
@@ -100,117 +107,20 @@ export function createWithHotkeys(
100
107
  }
101
108
  if (isArrowUp && editor.selection) {
102
109
  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(
110
+ const focusBlock = Node.descendant(
193
111
  editor,
194
- Path.next(editor.selection.focus.path.slice(0, 1)),
112
+ editor.selection.focus.path.slice(0, 1),
195
113
  ) 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
114
 
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)
115
+ if (isFirstBlock && focusBlock && Editor.isVoid(editor, focusBlock)) {
116
+ Transforms.insertNodes(
117
+ editor,
118
+ editor.pteCreateTextBlock({decorators: []}),
119
+ {
120
+ at: [0],
121
+ },
122
+ )
123
+ Transforms.select(editor, {path: [0, 0], offset: 0})
214
124
  editor.onChange()
215
125
  return
216
126
  }
@@ -220,7 +130,9 @@ export function createWithHotkeys(
220
130
  // 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
131
  // Otherwise tab is reserved for accessability for buttons etc.
222
132
  if ((isTab || isShiftTab) && editor.selection) {
223
- const [focusChild] = Editor.node(editor, editor.selection.focus, {depth: 2})
133
+ const [focusChild] = Editor.node(editor, editor.selection.focus, {
134
+ depth: 2,
135
+ })
224
136
  const [focusBlock] = isPortableTextSpan(focusChild)
225
137
  ? Editor.node(editor, editor.selection.focus, {depth: 1})
226
138
  : []
@@ -247,7 +159,9 @@ export function createWithHotkeys(
247
159
  // Deal with enter key combos
248
160
  if (isEnter && !isShiftEnter && editor.selection) {
249
161
  const focusBlockPath = editor.selection.focus.path.slice(0, 1)
250
- const focusBlock = Node.descendant(editor, focusBlockPath) as SlateTextBlock | VoidElement
162
+ const focusBlock = Node.descendant(editor, focusBlockPath) as
163
+ | SlateTextBlock
164
+ | VoidElement
251
165
 
252
166
  // List item enter key
253
167
  if (editor.isListBlock(focusBlock)) {
@@ -266,7 +180,10 @@ export function createWithHotkeys(
266
180
  const [, end] = Range.edges(editor.selection)
267
181
  const endAtEndOfNode = Editor.isEnd(editor, end, end.path)
268
182
  if (endAtEndOfNode) {
269
- Editor.insertNode(editor, editor.pteCreateEmptyBlock())
183
+ Editor.insertNode(
184
+ editor,
185
+ editor.pteCreateTextBlock({decorators: []}),
186
+ )
270
187
  event.preventDefault()
271
188
  editor.onChange()
272
189
  return
@@ -274,7 +191,7 @@ export function createWithHotkeys(
274
191
  }
275
192
  // Block object enter key
276
193
  if (focusBlock && Editor.isVoid(editor, focusBlock)) {
277
- Editor.insertNode(editor, editor.pteCreateEmptyBlock())
194
+ Editor.insertNode(editor, editor.pteCreateTextBlock({decorators: []}))
278
195
  event.preventDefault()
279
196
  editor.onChange()
280
197
  return
@@ -298,7 +215,10 @@ export function createWithHotkeys(
298
215
  editor.undo()
299
216
  return
300
217
  }
301
- if (isHotkey('mod+y', event.nativeEvent) || isHotkey('mod+shift+z', event.nativeEvent)) {
218
+ if (
219
+ isHotkey('mod+y', event.nativeEvent) ||
220
+ isHotkey('mod+shift+z', event.nativeEvent)
221
+ ) {
302
222
  event.preventDefault()
303
223
  editor.redo()
304
224
  }
@@ -1,45 +1,184 @@
1
+ import {isEqual} from 'lodash'
1
2
  import {Editor, Node, Path, Range, Transforms} from 'slate'
2
-
3
- import {type PortableTextMemberSchemaTypes, type PortableTextSlateEditor} from '../../types/editor'
3
+ import {
4
+ type PortableTextMemberSchemaTypes,
5
+ type PortableTextSlateEditor,
6
+ } from '../../types/editor'
4
7
  import {type SlateTextBlock, type VoidElement} from '../../types/slate'
5
- import {isEqualToEmptyEditor} from '../../utils/values'
6
8
 
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,9 +1,8 @@
1
1
  import {htmlToBlocks, normalizeBlock} from '@sanity/block-tools'
2
2
  import {type PortableTextBlock, type 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
6
  import {
8
7
  type EditorChanges,
9
8
  type PortableTextMemberSchemaTypes,
@@ -11,7 +10,11 @@ import {
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
 
@@ -180,7 +193,9 @@ export function createWithInsertData(
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
@@ -313,7 +336,9 @@ function _regenerateKeys(
313
336
  marks:
314
337
  child.marks && child.marks.includes(oldKey)
315
338
  ? // eslint-disable-next-line max-nested-callbacks
316
- [...child.marks].filter((mark) => mark !== oldKey).concat(newKey)
339
+ [...child.marks]
340
+ .filter((mark) => mark !== oldKey)
341
+ .concat(newKey)
317
342
  : child.marks,
318
343
  }
319
344
  : child,
@@ -347,22 +372,34 @@ function _insertFragment(
347
372
  return
348
373
  }
349
374
  // 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})
375
+ const [focusBlock, focusPath] = Editor.node(editor, editor.selection, {
376
+ depth: 1,
377
+ })
351
378
  if (editor.isTextBlock(focusBlock) && editor.isTextBlock(fragment[0])) {
352
379
  const {markDefs} = focusBlock
353
- debug('Mixing markDefs of focusBlock and fragments[0] block', markDefs, fragment[0].markDefs)
380
+ debug(
381
+ 'Mixing markDefs of focusBlock and fragments[0] block',
382
+ markDefs,
383
+ fragment[0].markDefs,
384
+ )
354
385
  if (!isEqual(markDefs, fragment[0].markDefs)) {
355
386
  Transforms.setNodes(
356
387
  editor,
357
388
  {
358
- markDefs: uniq([...(fragment[0].markDefs || []), ...(markDefs || [])]),
389
+ markDefs: uniq([
390
+ ...(fragment[0].markDefs || []),
391
+ ...(markDefs || []),
392
+ ]),
359
393
  },
360
394
  {at: focusPath, mode: 'lowest', voids: false},
361
395
  )
362
396
  }
363
397
  }
364
398
 
365
- const isPasteToEmptyEditor = isEqualToEmptyEditor(editor.children, schemaTypes)
399
+ const isPasteToEmptyEditor = isEqualToEmptyEditor(
400
+ editor.children,
401
+ schemaTypes,
402
+ )
366
403
 
367
404
  if (isPasteToEmptyEditor) {
368
405
  // Special case for pasting directly into an empty editor (a placeholder block).
@@ -7,7 +7,9 @@ import {isRedoing, isUndoing} from '../../utils/withUndoRedo'
7
7
  *
8
8
  */
9
9
  export function createWithMaxBlocks(maxBlocks: number) {
10
- return function withMaxBlocks(editor: PortableTextSlateEditor): PortableTextSlateEditor {
10
+ return function withMaxBlocks(
11
+ editor: PortableTextSlateEditor,
12
+ ): PortableTextSlateEditor {
11
13
  const {apply} = editor
12
14
  editor.apply = (operation) => {
13
15
  /**
@@ -31,7 +33,8 @@ export function createWithMaxBlocks(maxBlocks: number) {
31
33
  const rows = maxBlocks
32
34
  if (rows > 0 && editor.children.length >= rows) {
33
35
  if (
34
- (operation.type === 'insert_node' || operation.type === 'split_node') &&
36
+ (operation.type === 'insert_node' ||
37
+ operation.type === 'split_node') &&
35
38
  operation.path.length === 1
36
39
  ) {
37
40
  return
@@ -1,6 +1,8 @@
1
1
  import {Editor, Element, Node, Transforms} from 'slate'
2
-
3
- import {type PortableTextMemberSchemaTypes, type PortableTextSlateEditor} from '../../types/editor'
2
+ import {
3
+ type PortableTextMemberSchemaTypes,
4
+ type PortableTextSlateEditor,
5
+ } from '../../types/editor'
4
6
  import {isChangingRemotely} from '../../utils/withChanges'
5
7
  import {isRedoing, isUndoing} from '../../utils/withUndoRedo'
6
8
 
@@ -12,7 +14,9 @@ export function createWithObjectKeys(
12
14
  schemaTypes: PortableTextMemberSchemaTypes,
13
15
  keyGenerator: () => string,
14
16
  ) {
15
- return function withKeys(editor: PortableTextSlateEditor): PortableTextSlateEditor {
17
+ return function withKeys(
18
+ editor: PortableTextSlateEditor,
19
+ ): PortableTextSlateEditor {
16
20
  const {apply, normalizeNode} = editor
17
21
 
18
22
  // The default behavior is to always generate a new key here.