@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.
- package/lib/index.d.mts +142 -67
- package/lib/index.d.ts +142 -67
- package/lib/index.esm.js +1130 -371
- package/lib/index.esm.js.map +1 -1
- package/lib/index.js +1130 -371
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +1130 -371
- package/lib/index.mjs.map +1 -1
- package/package.json +4 -18
- package/src/editor/Editable.tsx +128 -55
- package/src/editor/PortableTextEditor.tsx +66 -32
- package/src/editor/__tests__/PortableTextEditor.test.tsx +44 -18
- package/src/editor/__tests__/PortableTextEditorTester.tsx +50 -38
- package/src/editor/__tests__/RangeDecorations.test.tsx +4 -6
- package/src/editor/__tests__/handleClick.test.tsx +28 -9
- package/src/editor/__tests__/insert-block.test.tsx +24 -8
- package/src/editor/__tests__/pteWarningsSelfSolving.test.tsx +31 -63
- package/src/editor/__tests__/utils.ts +10 -4
- package/src/editor/components/DraggableBlock.tsx +36 -13
- package/src/editor/components/Element.tsx +73 -33
- package/src/editor/components/Leaf.tsx +114 -76
- package/src/editor/components/SlateContainer.tsx +14 -7
- package/src/editor/components/Synchronizer.tsx +8 -5
- package/src/editor/hooks/usePortableTextEditor.ts +3 -3
- package/src/editor/hooks/usePortableTextEditorSelection.tsx +10 -4
- package/src/editor/hooks/useSyncValue.test.tsx +9 -4
- package/src/editor/hooks/useSyncValue.ts +198 -133
- package/src/editor/nodes/DefaultAnnotation.tsx +6 -4
- package/src/editor/nodes/DefaultObject.tsx +1 -1
- package/src/editor/plugins/__tests__/createWithInsertData.test.tsx +23 -8
- 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 +5 -3
- package/src/editor/plugins/__tests__/withPortableTextLists.test.tsx +4 -2
- package/src/editor/plugins/__tests__/withPortableTextMarkModel.test.tsx +61 -19
- 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 +361 -131
- package/src/editor/plugins/createWithHotKeys.ts +46 -130
- package/src/editor/plugins/createWithInsertBreak.ts +167 -28
- package/src/editor/plugins/createWithInsertData.ts +66 -30
- package/src/editor/plugins/createWithMaxBlocks.ts +6 -3
- package/src/editor/plugins/createWithObjectKeys.ts +7 -3
- package/src/editor/plugins/createWithPatches.ts +66 -24
- package/src/editor/plugins/createWithPlaceholderBlock.ts +9 -5
- package/src/editor/plugins/createWithPortableTextBlockStyle.ts +17 -7
- package/src/editor/plugins/createWithPortableTextLists.ts +21 -9
- package/src/editor/plugins/createWithPortableTextMarkModel.ts +217 -52
- package/src/editor/plugins/createWithPortableTextSelections.ts +11 -9
- package/src/editor/plugins/createWithSchemaTypes.ts +26 -10
- package/src/editor/plugins/createWithUndoRedo.ts +106 -27
- package/src/editor/plugins/createWithUtils.ts +33 -11
- package/src/editor/plugins/index.ts +34 -13
- package/src/types/editor.ts +73 -44
- package/src/types/options.ts +7 -5
- package/src/types/slate.ts +6 -6
- package/src/utils/__tests__/dmpToOperations.test.ts +41 -16
- package/src/utils/__tests__/operationToPatches.test.ts +4 -3
- package/src/utils/__tests__/patchToOperations.test.ts +16 -5
- package/src/utils/__tests__/ranges.test.ts +9 -4
- package/src/utils/__tests__/valueNormalization.test.tsx +12 -4
- package/src/utils/__tests__/values.test.ts +0 -1
- package/src/utils/applyPatch.ts +78 -29
- package/src/utils/getPortableTextMemberSchemaTypes.ts +38 -23
- package/src/utils/operationToPatches.ts +123 -44
- package/src/utils/paths.ts +26 -9
- package/src/utils/ranges.ts +16 -10
- package/src/utils/selection.ts +21 -9
- package/src/utils/ucs2Indices.ts +2 -2
- package/src/utils/validateValue.ts +118 -45
- package/src/utils/values.ts +38 -17
- package/src/utils/weakMaps.ts +20 -10
- package/src/utils/withChanges.ts +5 -3
- package/src/utils/withUndoRedo.ts +1 -1
- 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
|
|
3
|
+
import type {KeyboardEvent} from 'react'
|
|
6
4
|
import {Editor, Node, Path, Range, Transforms} from 'slate'
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
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(
|
|
88
|
-
|
|
89
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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, {
|
|
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
|
|
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(
|
|
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.
|
|
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 (
|
|
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(
|
|
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,17 +1,20 @@
|
|
|
1
1
|
import {htmlToBlocks, normalizeBlock} from '@sanity/block-tools'
|
|
2
|
-
import
|
|
2
|
+
import type {PortableTextBlock, 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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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 {
|
|
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
|
|
|
@@ -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) =>
|
|
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
|
|
@@ -312,8 +335,9 @@ function _regenerateKeys(
|
|
|
312
335
|
...child,
|
|
313
336
|
marks:
|
|
314
337
|
child.marks && child.marks.includes(oldKey)
|
|
315
|
-
?
|
|
316
|
-
|
|
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, {
|
|
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(
|
|
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([
|
|
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(
|
|
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).
|