@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.
- package/lib/index.d.mts +140 -66
- package/lib/index.d.ts +140 -66
- package/lib/index.esm.js +1164 -410
- package/lib/index.esm.js.map +1 -1
- package/lib/index.js +1164 -410
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +1164 -410
- package/lib/index.mjs.map +1 -1
- package/package.json +8 -4
- package/src/editor/Editable.tsx +107 -36
- package/src/editor/PortableTextEditor.tsx +47 -12
- package/src/editor/__tests__/PortableTextEditor.test.tsx +42 -15
- package/src/editor/__tests__/PortableTextEditorTester.tsx +50 -38
- package/src/editor/__tests__/RangeDecorations.test.tsx +0 -1
- package/src/editor/__tests__/handleClick.test.tsx +28 -9
- package/src/editor/__tests__/insert-block.test.tsx +22 -6
- package/src/editor/__tests__/pteWarningsSelfSolving.test.tsx +30 -62
- package/src/editor/__tests__/utils.ts +10 -3
- package/src/editor/components/DraggableBlock.tsx +36 -13
- package/src/editor/components/Element.tsx +59 -17
- package/src/editor/components/Leaf.tsx +106 -68
- package/src/editor/components/SlateContainer.tsx +12 -5
- package/src/editor/components/Synchronizer.tsx +5 -2
- package/src/editor/hooks/usePortableTextEditor.ts +2 -2
- package/src/editor/hooks/usePortableTextEditorSelection.tsx +9 -3
- package/src/editor/hooks/useSyncValue.test.tsx +9 -4
- package/src/editor/hooks/useSyncValue.ts +199 -130
- package/src/editor/nodes/DefaultAnnotation.tsx +6 -3
- package/src/editor/plugins/__tests__/createWithInsertData.test.tsx +25 -7
- package/src/editor/plugins/__tests__/withEditableAPIDelete.test.tsx +26 -9
- package/src/editor/plugins/__tests__/withEditableAPIGetFragment.test.tsx +15 -5
- package/src/editor/plugins/__tests__/withEditableAPIInsert.test.tsx +60 -19
- package/src/editor/plugins/__tests__/withEditableAPISelectionsOverlapping.test.tsx +4 -2
- package/src/editor/plugins/__tests__/withPortableTextLists.test.tsx +4 -2
- package/src/editor/plugins/__tests__/withPortableTextMarkModel.test.tsx +61 -550
- package/src/editor/plugins/__tests__/withPortableTextSelections.test.tsx +6 -3
- package/src/editor/plugins/__tests__/withUndoRedo.test.tsx +30 -13
- package/src/editor/plugins/createWithEditableAPI.ts +354 -115
- package/src/editor/plugins/createWithHotKeys.ts +41 -121
- package/src/editor/plugins/createWithInsertBreak.ts +166 -27
- package/src/editor/plugins/createWithInsertData.ts +60 -23
- package/src/editor/plugins/createWithMaxBlocks.ts +5 -2
- package/src/editor/plugins/createWithObjectKeys.ts +7 -3
- package/src/editor/plugins/createWithPatches.ts +60 -16
- package/src/editor/plugins/createWithPlaceholderBlock.ts +7 -3
- package/src/editor/plugins/createWithPortableTextBlockStyle.ts +17 -7
- package/src/editor/plugins/createWithPortableTextLists.ts +21 -8
- package/src/editor/plugins/createWithPortableTextMarkModel.ts +301 -155
- package/src/editor/plugins/createWithPortableTextSelections.ts +4 -2
- package/src/editor/plugins/createWithSchemaTypes.ts +25 -9
- package/src/editor/plugins/createWithUndoRedo.ts +107 -24
- package/src/editor/plugins/createWithUtils.ts +32 -10
- package/src/editor/plugins/index.ts +31 -10
- package/src/types/editor.ts +44 -15
- package/src/types/options.ts +4 -2
- package/src/types/slate.ts +2 -2
- package/src/utils/__tests__/dmpToOperations.test.ts +38 -13
- package/src/utils/__tests__/operationToPatches.test.ts +3 -2
- package/src/utils/__tests__/patchToOperations.test.ts +15 -4
- package/src/utils/__tests__/ranges.test.ts +8 -3
- package/src/utils/__tests__/valueNormalization.test.tsx +12 -4
- package/src/utils/__tests__/values.test.ts +0 -1
- package/src/utils/applyPatch.ts +71 -20
- package/src/utils/getPortableTextMemberSchemaTypes.ts +30 -15
- package/src/utils/operationToPatches.ts +126 -43
- package/src/utils/paths.ts +24 -7
- package/src/utils/ranges.ts +12 -5
- package/src/utils/selection.ts +19 -7
- package/src/utils/validateValue.ts +118 -45
- package/src/utils/values.ts +31 -10
- package/src/utils/weakMaps.ts +18 -8
- package/src/utils/withChanges.ts +4 -2
- package/src/editor/plugins/__tests__/withHotkeys.test.tsx +0 -212
- package/src/editor/plugins/__tests__/withInsertBreak.test.tsx +0 -220
- 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
|
-
|
|
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(
|
|
88
|
-
|
|
89
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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, {
|
|
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
|
|
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(
|
|
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.
|
|
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 (
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
37
|
-
|
|
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 {
|
|
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 {
|
|
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(
|
|
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 =
|
|
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)
|
|
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)
|
|
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(
|
|
86
|
-
|
|
87
|
-
|
|
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(
|
|
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) =>
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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]
|
|
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, {
|
|
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(
|
|
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([
|
|
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(
|
|
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(
|
|
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' ||
|
|
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
|
-
|
|
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(
|
|
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.
|