@portabletext/editor 0.0.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/LICENSE +21 -0
- package/README.md +3 -0
- package/lib/index.d.mts +911 -0
- package/lib/index.d.ts +911 -0
- package/lib/index.esm.js +4896 -0
- package/lib/index.esm.js.map +1 -0
- package/lib/index.js +4874 -0
- package/lib/index.js.map +1 -0
- package/lib/index.mjs +4896 -0
- package/lib/index.mjs.map +1 -0
- package/package.json +119 -0
- package/src/editor/Editable.tsx +683 -0
- package/src/editor/PortableTextEditor.tsx +308 -0
- package/src/editor/__tests__/PortableTextEditor.test.tsx +386 -0
- package/src/editor/__tests__/PortableTextEditorTester.tsx +116 -0
- package/src/editor/__tests__/RangeDecorations.test.tsx +115 -0
- package/src/editor/__tests__/handleClick.test.tsx +218 -0
- package/src/editor/__tests__/pteWarningsSelfSolving.test.tsx +389 -0
- package/src/editor/__tests__/utils.ts +39 -0
- package/src/editor/components/DraggableBlock.tsx +287 -0
- package/src/editor/components/Element.tsx +279 -0
- package/src/editor/components/Leaf.tsx +288 -0
- package/src/editor/components/SlateContainer.tsx +81 -0
- package/src/editor/components/Synchronizer.tsx +190 -0
- package/src/editor/hooks/usePortableTextEditor.ts +23 -0
- package/src/editor/hooks/usePortableTextEditorKeyGenerator.ts +24 -0
- package/src/editor/hooks/usePortableTextEditorSelection.ts +22 -0
- package/src/editor/hooks/usePortableTextEditorValue.ts +16 -0
- package/src/editor/hooks/usePortableTextReadOnly.ts +20 -0
- package/src/editor/hooks/useSyncValue.test.tsx +125 -0
- package/src/editor/hooks/useSyncValue.ts +372 -0
- package/src/editor/nodes/DefaultAnnotation.tsx +16 -0
- package/src/editor/nodes/DefaultObject.tsx +15 -0
- package/src/editor/nodes/index.ts +189 -0
- package/src/editor/plugins/__tests__/withEditableAPIDelete.test.tsx +244 -0
- package/src/editor/plugins/__tests__/withEditableAPIGetFragment.test.tsx +142 -0
- package/src/editor/plugins/__tests__/withEditableAPIInsert.test.tsx +346 -0
- package/src/editor/plugins/__tests__/withEditableAPISelectionsOverlapping.test.tsx +162 -0
- package/src/editor/plugins/__tests__/withHotkeys.test.tsx +212 -0
- package/src/editor/plugins/__tests__/withInsertBreak.test.tsx +204 -0
- package/src/editor/plugins/__tests__/withPlaceholderBlock.test.tsx +133 -0
- package/src/editor/plugins/__tests__/withPortableTextLists.test.tsx +65 -0
- package/src/editor/plugins/__tests__/withPortableTextMarkModel.test.tsx +1377 -0
- package/src/editor/plugins/__tests__/withPortableTextSelections.test.tsx +91 -0
- package/src/editor/plugins/__tests__/withUndoRedo.test.tsx +115 -0
- package/src/editor/plugins/createWithEditableAPI.ts +573 -0
- package/src/editor/plugins/createWithHotKeys.ts +304 -0
- package/src/editor/plugins/createWithInsertBreak.ts +45 -0
- package/src/editor/plugins/createWithInsertData.ts +359 -0
- package/src/editor/plugins/createWithMaxBlocks.ts +24 -0
- package/src/editor/plugins/createWithObjectKeys.ts +63 -0
- package/src/editor/plugins/createWithPatches.ts +274 -0
- package/src/editor/plugins/createWithPlaceholderBlock.ts +36 -0
- package/src/editor/plugins/createWithPortableTextBlockStyle.ts +91 -0
- package/src/editor/plugins/createWithPortableTextLists.ts +160 -0
- package/src/editor/plugins/createWithPortableTextMarkModel.ts +441 -0
- package/src/editor/plugins/createWithPortableTextSelections.ts +65 -0
- package/src/editor/plugins/createWithSchemaTypes.ts +76 -0
- package/src/editor/plugins/createWithUndoRedo.ts +494 -0
- package/src/editor/plugins/createWithUtils.ts +81 -0
- package/src/editor/plugins/index.ts +155 -0
- package/src/index.ts +11 -0
- package/src/patch/PatchEvent.ts +33 -0
- package/src/patch/applyPatch.ts +29 -0
- package/src/patch/array.ts +89 -0
- package/src/patch/arrayInsert.ts +27 -0
- package/src/patch/object.ts +39 -0
- package/src/patch/patches.ts +53 -0
- package/src/patch/primitive.ts +43 -0
- package/src/patch/string.ts +51 -0
- package/src/types/editor.ts +576 -0
- package/src/types/options.ts +17 -0
- package/src/types/patch.ts +65 -0
- package/src/types/slate.ts +25 -0
- package/src/utils/__tests__/dmpToOperations.test.ts +181 -0
- package/src/utils/__tests__/operationToPatches.test.ts +421 -0
- package/src/utils/__tests__/patchToOperations.test.ts +293 -0
- package/src/utils/__tests__/ranges.test.ts +18 -0
- package/src/utils/__tests__/valueNormalization.test.tsx +62 -0
- package/src/utils/__tests__/values.test.ts +253 -0
- package/src/utils/applyPatch.ts +407 -0
- package/src/utils/bufferUntil.ts +15 -0
- package/src/utils/debug.ts +12 -0
- package/src/utils/getPortableTextMemberSchemaTypes.ts +100 -0
- package/src/utils/operationToPatches.ts +357 -0
- package/src/utils/patches.ts +36 -0
- package/src/utils/paths.ts +60 -0
- package/src/utils/ranges.ts +77 -0
- package/src/utils/schema.ts +8 -0
- package/src/utils/selection.ts +65 -0
- package/src/utils/ucs2Indices.ts +67 -0
- package/src/utils/validateValue.ts +394 -0
- package/src/utils/values.ts +208 -0
- package/src/utils/weakMaps.ts +24 -0
- package/src/utils/withChanges.ts +25 -0
- package/src/utils/withPreserveKeys.ts +14 -0
- package/src/utils/withoutPatching.ts +14 -0
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import {htmlToBlocks, normalizeBlock} from '@sanity/block-tools'
|
|
2
|
+
import {type PortableTextBlock, type PortableTextChild} from '@sanity/types'
|
|
3
|
+
import {isEqual, uniq} from 'lodash'
|
|
4
|
+
import {type Descendant, Editor, type Node, Range, Transforms} from 'slate'
|
|
5
|
+
import {ReactEditor} from 'slate-react'
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
type EditorChanges,
|
|
9
|
+
type PortableTextMemberSchemaTypes,
|
|
10
|
+
type PortableTextSlateEditor,
|
|
11
|
+
} from '../../types/editor'
|
|
12
|
+
import {debugWithName} from '../../utils/debug'
|
|
13
|
+
import {validateValue} from '../../utils/validateValue'
|
|
14
|
+
import {fromSlateValue, isEqualToEmptyEditor, toSlateValue} from '../../utils/values'
|
|
15
|
+
|
|
16
|
+
const debug = debugWithName('plugin:withInsertData')
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* This plugin handles copy/paste in the editor
|
|
20
|
+
*
|
|
21
|
+
*/
|
|
22
|
+
export function createWithInsertData(
|
|
23
|
+
change$: EditorChanges,
|
|
24
|
+
schemaTypes: PortableTextMemberSchemaTypes,
|
|
25
|
+
keyGenerator: () => string,
|
|
26
|
+
) {
|
|
27
|
+
return function withInsertData(editor: PortableTextSlateEditor): PortableTextSlateEditor {
|
|
28
|
+
const blockTypeName = schemaTypes.block.name
|
|
29
|
+
const spanTypeName = schemaTypes.span.name
|
|
30
|
+
const whitespaceOnPasteMode = schemaTypes.block.options.unstable_whitespaceOnPasteMode
|
|
31
|
+
|
|
32
|
+
const toPlainText = (blocks: PortableTextBlock[]) => {
|
|
33
|
+
return blocks
|
|
34
|
+
.map((block) => {
|
|
35
|
+
if (editor.isTextBlock(block)) {
|
|
36
|
+
return block.children
|
|
37
|
+
.map((child: PortableTextChild) => {
|
|
38
|
+
if (child._type === spanTypeName) {
|
|
39
|
+
return child.text
|
|
40
|
+
}
|
|
41
|
+
return `[${
|
|
42
|
+
schemaTypes.inlineObjects.find((t) => t.name === child._type)?.title || 'Object'
|
|
43
|
+
}]`
|
|
44
|
+
})
|
|
45
|
+
.join('')
|
|
46
|
+
}
|
|
47
|
+
return `[${
|
|
48
|
+
schemaTypes.blockObjects.find((t) => t.name === block._type)?.title || 'Object'
|
|
49
|
+
}]`
|
|
50
|
+
})
|
|
51
|
+
.join('\n\n')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
editor.setFragmentData = (data: DataTransfer, originEvent) => {
|
|
55
|
+
const {selection} = editor
|
|
56
|
+
|
|
57
|
+
if (!selection) {
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const [start, end] = Range.edges(selection)
|
|
62
|
+
const startVoid = Editor.void(editor, {at: start.path})
|
|
63
|
+
const endVoid = Editor.void(editor, {at: end.path})
|
|
64
|
+
|
|
65
|
+
if (Range.isCollapsed(selection) && !startVoid) {
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Create a fake selection so that we can add a Base64-encoded copy of the
|
|
70
|
+
// fragment to the HTML, to decode on future pastes.
|
|
71
|
+
const domRange = ReactEditor.toDOMRange(editor, selection)
|
|
72
|
+
let contents = domRange.cloneContents()
|
|
73
|
+
// COMPAT: If the end node is a void node, we need to move the end of the
|
|
74
|
+
// range from the void node's spacer span, to the end of the void node's
|
|
75
|
+
// content, since the spacer is before void's content in the DOM.
|
|
76
|
+
if (endVoid) {
|
|
77
|
+
const [voidNode] = endVoid
|
|
78
|
+
const r = domRange.cloneRange()
|
|
79
|
+
const domNode = ReactEditor.toDOMNode(editor, voidNode)
|
|
80
|
+
r.setEndAfter(domNode)
|
|
81
|
+
contents = r.cloneContents()
|
|
82
|
+
}
|
|
83
|
+
// Remove any zero-width space spans from the cloned DOM so that they don't
|
|
84
|
+
// 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
|
+
})
|
|
89
|
+
// Clean up the clipboard HTML for editor spesific attributes
|
|
90
|
+
Array.from(contents.querySelectorAll('*')).forEach((elm) => {
|
|
91
|
+
elm.removeAttribute('contentEditable')
|
|
92
|
+
elm.removeAttribute('data-slate-inline')
|
|
93
|
+
elm.removeAttribute('data-slate-leaf')
|
|
94
|
+
elm.removeAttribute('data-slate-node')
|
|
95
|
+
elm.removeAttribute('data-slate-spacer')
|
|
96
|
+
elm.removeAttribute('data-slate-string')
|
|
97
|
+
elm.removeAttribute('data-slate-zero-width')
|
|
98
|
+
elm.removeAttribute('draggable')
|
|
99
|
+
for (const key in elm.attributes) {
|
|
100
|
+
if (elm.hasAttribute(key)) {
|
|
101
|
+
elm.removeAttribute(key)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
const div = contents.ownerDocument.createElement('div')
|
|
106
|
+
div.appendChild(contents)
|
|
107
|
+
div.setAttribute('hidden', 'true')
|
|
108
|
+
contents.ownerDocument.body.appendChild(div)
|
|
109
|
+
const asHTML = div.innerHTML
|
|
110
|
+
contents.ownerDocument.body.removeChild(div)
|
|
111
|
+
const fragment = editor.getFragment()
|
|
112
|
+
const portableText = fromSlateValue(fragment, blockTypeName)
|
|
113
|
+
|
|
114
|
+
const asJSON = JSON.stringify(portableText)
|
|
115
|
+
const asPlainText = toPlainText(portableText)
|
|
116
|
+
data.clearData()
|
|
117
|
+
data.setData('text/plain', asPlainText)
|
|
118
|
+
data.setData('text/html', asHTML)
|
|
119
|
+
data.setData('application/json', asJSON)
|
|
120
|
+
data.setData('application/x-portable-text', asJSON)
|
|
121
|
+
debug('text', asPlainText)
|
|
122
|
+
data.setData('application/x-portable-text-event-origin', originEvent || 'external')
|
|
123
|
+
debug('Set fragment data', asJSON, asHTML)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
editor.insertPortableTextData = (data: DataTransfer): boolean => {
|
|
127
|
+
if (!editor.selection) {
|
|
128
|
+
return false
|
|
129
|
+
}
|
|
130
|
+
const pText = data.getData('application/x-portable-text')
|
|
131
|
+
const origin = data.getData('application/x-portable-text-event-origin')
|
|
132
|
+
debug(`Inserting portable text from ${origin} event`, pText)
|
|
133
|
+
if (pText) {
|
|
134
|
+
const parsed = JSON.parse(pText) as PortableTextBlock[]
|
|
135
|
+
if (Array.isArray(parsed) && parsed.length > 0) {
|
|
136
|
+
const slateValue = _regenerateKeys(
|
|
137
|
+
editor,
|
|
138
|
+
toSlateValue(parsed, {schemaTypes}),
|
|
139
|
+
keyGenerator,
|
|
140
|
+
spanTypeName,
|
|
141
|
+
)
|
|
142
|
+
// Validate the result
|
|
143
|
+
const validation = validateValue(parsed, schemaTypes, keyGenerator)
|
|
144
|
+
// Bail out if it's not valid
|
|
145
|
+
if (!validation.valid && !validation.resolution?.autoResolve) {
|
|
146
|
+
const errorDescription = `${validation.resolution?.description}`
|
|
147
|
+
change$.next({
|
|
148
|
+
type: 'error',
|
|
149
|
+
level: 'warning',
|
|
150
|
+
name: 'pasteError',
|
|
151
|
+
description: errorDescription,
|
|
152
|
+
data: validation,
|
|
153
|
+
})
|
|
154
|
+
debug('Invalid insert result', validation)
|
|
155
|
+
return false
|
|
156
|
+
}
|
|
157
|
+
_insertFragment(editor, slateValue, schemaTypes)
|
|
158
|
+
return true
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return false
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
editor.insertTextOrHTMLData = (data: DataTransfer): boolean => {
|
|
165
|
+
if (!editor.selection) {
|
|
166
|
+
debug('No selection, not inserting')
|
|
167
|
+
return false
|
|
168
|
+
}
|
|
169
|
+
change$.next({type: 'loading', isLoading: true}) // This could potentially take some time
|
|
170
|
+
const html = data.getData('text/html')
|
|
171
|
+
const text = data.getData('text/plain')
|
|
172
|
+
|
|
173
|
+
if (html || text) {
|
|
174
|
+
debug('Inserting data', data)
|
|
175
|
+
let portableText: PortableTextBlock[]
|
|
176
|
+
let fragment: Node[]
|
|
177
|
+
let insertedType
|
|
178
|
+
|
|
179
|
+
if (html) {
|
|
180
|
+
portableText = htmlToBlocks(html, schemaTypes.portableText, {
|
|
181
|
+
unstable_whitespaceOnPasteMode: whitespaceOnPasteMode,
|
|
182
|
+
}).map((block) => normalizeBlock(block, {blockTypeName})) as PortableTextBlock[]
|
|
183
|
+
fragment = toSlateValue(portableText, {schemaTypes})
|
|
184
|
+
insertedType = 'HTML'
|
|
185
|
+
|
|
186
|
+
if (portableText.length === 0) {
|
|
187
|
+
return false
|
|
188
|
+
}
|
|
189
|
+
} else {
|
|
190
|
+
// plain text
|
|
191
|
+
const blocks = escapeHtml(text)
|
|
192
|
+
.split(/\n{2,}/)
|
|
193
|
+
.map((line) =>
|
|
194
|
+
line ? `<p>${line.replace(/(?:\r\n|\r|\n)/g, '<br/>')}</p>` : '<p></p>',
|
|
195
|
+
)
|
|
196
|
+
.join('')
|
|
197
|
+
const textToHtml = `<html><body>${blocks}</body></html>`
|
|
198
|
+
portableText = htmlToBlocks(textToHtml, schemaTypes.portableText).map((block) =>
|
|
199
|
+
normalizeBlock(block, {blockTypeName}),
|
|
200
|
+
) as PortableTextBlock[]
|
|
201
|
+
fragment = toSlateValue(portableText, {
|
|
202
|
+
schemaTypes,
|
|
203
|
+
})
|
|
204
|
+
insertedType = 'text'
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Validate the result
|
|
208
|
+
const validation = validateValue(portableText, schemaTypes, keyGenerator)
|
|
209
|
+
|
|
210
|
+
// Bail out if it's not valid
|
|
211
|
+
if (!validation.valid) {
|
|
212
|
+
const errorDescription = `Could not validate the resulting portable text to insert.\n${validation.resolution?.description}\nTry to insert as plain text (shift-paste) instead.`
|
|
213
|
+
change$.next({
|
|
214
|
+
type: 'error',
|
|
215
|
+
level: 'warning',
|
|
216
|
+
name: 'pasteError',
|
|
217
|
+
description: errorDescription,
|
|
218
|
+
data: validation,
|
|
219
|
+
})
|
|
220
|
+
debug('Invalid insert result', validation)
|
|
221
|
+
return false
|
|
222
|
+
}
|
|
223
|
+
debug(`Inserting ${insertedType} fragment at ${JSON.stringify(editor.selection)}`)
|
|
224
|
+
_insertFragment(editor, fragment, schemaTypes)
|
|
225
|
+
change$.next({type: 'loading', isLoading: false})
|
|
226
|
+
return true
|
|
227
|
+
}
|
|
228
|
+
change$.next({type: 'loading', isLoading: false})
|
|
229
|
+
return false
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
editor.insertData = (data: DataTransfer) => {
|
|
233
|
+
if (!editor.insertPortableTextData(data)) {
|
|
234
|
+
editor.insertTextOrHTMLData(data)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
editor.insertFragmentData = (data: DataTransfer): boolean => {
|
|
239
|
+
const fragment = data.getData('application/x-portable-text')
|
|
240
|
+
if (fragment) {
|
|
241
|
+
const parsed = JSON.parse(fragment)
|
|
242
|
+
editor.insertFragment(parsed)
|
|
243
|
+
return true
|
|
244
|
+
}
|
|
245
|
+
return false
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return editor
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const entityMap: Record<string, string> = {
|
|
253
|
+
'&': '&',
|
|
254
|
+
'<': '<',
|
|
255
|
+
'>': '>',
|
|
256
|
+
'"': '"',
|
|
257
|
+
"'": ''',
|
|
258
|
+
'/': '/',
|
|
259
|
+
'`': '`',
|
|
260
|
+
'=': '=',
|
|
261
|
+
}
|
|
262
|
+
function escapeHtml(str: string) {
|
|
263
|
+
return String(str).replace(/[&<>"'`=/]/g, (s: string) => entityMap[s])
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Shared helper function to regenerate the keys on a fragment.
|
|
268
|
+
*
|
|
269
|
+
* @internal
|
|
270
|
+
*/
|
|
271
|
+
function _regenerateKeys(
|
|
272
|
+
editor: PortableTextSlateEditor,
|
|
273
|
+
fragment: Descendant[],
|
|
274
|
+
keyGenerator: () => string,
|
|
275
|
+
spanTypeName: string,
|
|
276
|
+
): Descendant[] {
|
|
277
|
+
return fragment.map((node) => {
|
|
278
|
+
const newNode: Descendant = {...node}
|
|
279
|
+
// Ensure the copy has new keys
|
|
280
|
+
if (editor.isTextBlock(newNode)) {
|
|
281
|
+
newNode.markDefs = (newNode.markDefs || []).map((def) => {
|
|
282
|
+
const oldKey = def._key
|
|
283
|
+
const newKey = keyGenerator()
|
|
284
|
+
newNode.children = newNode.children.map((child) =>
|
|
285
|
+
child._type === spanTypeName && editor.isTextSpan(child)
|
|
286
|
+
? {
|
|
287
|
+
...child,
|
|
288
|
+
marks:
|
|
289
|
+
child.marks && child.marks.includes(oldKey)
|
|
290
|
+
? // eslint-disable-next-line max-nested-callbacks
|
|
291
|
+
[...child.marks].filter((mark) => mark !== oldKey).concat(newKey)
|
|
292
|
+
: child.marks,
|
|
293
|
+
}
|
|
294
|
+
: child,
|
|
295
|
+
)
|
|
296
|
+
return {...def, _key: newKey}
|
|
297
|
+
})
|
|
298
|
+
}
|
|
299
|
+
const nodeWithNewKeys = {...newNode, _key: keyGenerator()}
|
|
300
|
+
if (editor.isTextBlock(nodeWithNewKeys)) {
|
|
301
|
+
nodeWithNewKeys.children = nodeWithNewKeys.children.map((child) => ({
|
|
302
|
+
...child,
|
|
303
|
+
_key: keyGenerator(),
|
|
304
|
+
}))
|
|
305
|
+
}
|
|
306
|
+
return nodeWithNewKeys as Descendant
|
|
307
|
+
})
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Shared helper function to insert the final fragment into the editor
|
|
312
|
+
*
|
|
313
|
+
* @internal
|
|
314
|
+
*/
|
|
315
|
+
function _insertFragment(
|
|
316
|
+
editor: PortableTextSlateEditor,
|
|
317
|
+
fragment: Descendant[],
|
|
318
|
+
schemaTypes: PortableTextMemberSchemaTypes,
|
|
319
|
+
) {
|
|
320
|
+
editor.withoutNormalizing(() => {
|
|
321
|
+
if (!editor.selection) {
|
|
322
|
+
return
|
|
323
|
+
}
|
|
324
|
+
// Ensure that markDefs for any annotations inside this fragment are copied over to the focused text block.
|
|
325
|
+
const [focusBlock, focusPath] = Editor.node(editor, editor.selection, {depth: 1})
|
|
326
|
+
if (editor.isTextBlock(focusBlock) && editor.isTextBlock(fragment[0])) {
|
|
327
|
+
const {markDefs} = focusBlock
|
|
328
|
+
debug('Mixing markDefs of focusBlock and fragments[0] block', markDefs, fragment[0].markDefs)
|
|
329
|
+
if (!isEqual(markDefs, fragment[0].markDefs)) {
|
|
330
|
+
Transforms.setNodes(
|
|
331
|
+
editor,
|
|
332
|
+
{
|
|
333
|
+
markDefs: uniq([...(fragment[0].markDefs || []), ...(markDefs || [])]),
|
|
334
|
+
},
|
|
335
|
+
{at: focusPath, mode: 'lowest', voids: false},
|
|
336
|
+
)
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const isPasteToEmptyEditor = isEqualToEmptyEditor(editor.children, schemaTypes)
|
|
341
|
+
|
|
342
|
+
if (isPasteToEmptyEditor) {
|
|
343
|
+
// Special case for pasting directly into an empty editor (a placeholder block).
|
|
344
|
+
// When pasting content starting with multiple empty blocks,
|
|
345
|
+
// `editor.insertFragment` can potentially duplicate the keys of
|
|
346
|
+
// the placeholder block because of operations that happen
|
|
347
|
+
// inside `editor.insertFragment` (involves an `insert_node` operation).
|
|
348
|
+
// However by splitting the placeholder block first in this situation we are good.
|
|
349
|
+
Transforms.splitNodes(editor, {at: [0, 0]})
|
|
350
|
+
editor.insertFragment(fragment)
|
|
351
|
+
Transforms.removeNodes(editor, {at: [0]})
|
|
352
|
+
} else {
|
|
353
|
+
// All other inserts
|
|
354
|
+
editor.insertFragment(fragment)
|
|
355
|
+
}
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
editor.onChange()
|
|
359
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {type PortableTextSlateEditor} from '../../types/editor'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* This plugin makes sure that the PTE maxBlocks prop is respected
|
|
5
|
+
*
|
|
6
|
+
*/
|
|
7
|
+
export function createWithMaxBlocks(maxBlocks: number) {
|
|
8
|
+
return function withMaxBlocks(editor: PortableTextSlateEditor): PortableTextSlateEditor {
|
|
9
|
+
const {apply} = editor
|
|
10
|
+
editor.apply = (operation) => {
|
|
11
|
+
const rows = maxBlocks
|
|
12
|
+
if (rows > 0 && editor.children.length >= rows) {
|
|
13
|
+
if (
|
|
14
|
+
(operation.type === 'insert_node' || operation.type === 'split_node') &&
|
|
15
|
+
operation.path.length === 1
|
|
16
|
+
) {
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
apply(operation)
|
|
21
|
+
}
|
|
22
|
+
return editor
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import {Editor, Element, Node, Transforms} from 'slate'
|
|
2
|
+
|
|
3
|
+
import {type PortableTextMemberSchemaTypes, type PortableTextSlateEditor} from '../../types/editor'
|
|
4
|
+
import {isPreservingKeys, PRESERVE_KEYS} from '../../utils/withPreserveKeys'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* This plugin makes sure that every new node in the editor get a new _key prop when created
|
|
8
|
+
*
|
|
9
|
+
*/
|
|
10
|
+
export function createWithObjectKeys(
|
|
11
|
+
schemaTypes: PortableTextMemberSchemaTypes,
|
|
12
|
+
keyGenerator: () => string,
|
|
13
|
+
) {
|
|
14
|
+
return function withKeys(editor: PortableTextSlateEditor): PortableTextSlateEditor {
|
|
15
|
+
PRESERVE_KEYS.set(editor, false)
|
|
16
|
+
const {apply, normalizeNode} = editor
|
|
17
|
+
|
|
18
|
+
// The apply function can be called with a scope (withPreserveKeys) that will
|
|
19
|
+
// preserve keys for the produced nodes if they have a _key property set already.
|
|
20
|
+
// The default behavior is to always generate a new key here.
|
|
21
|
+
// For example, when undoing and redoing we want to retain the keys, but
|
|
22
|
+
// when we create a new bold span by splitting a non-bold-span we want the produced node to get a new key.
|
|
23
|
+
editor.apply = (operation) => {
|
|
24
|
+
if (operation.type === 'split_node') {
|
|
25
|
+
const withNewKey = !isPreservingKeys(editor) || !('_key' in operation.properties)
|
|
26
|
+
operation.properties = {
|
|
27
|
+
...operation.properties,
|
|
28
|
+
...(withNewKey ? {_key: keyGenerator()} : {}),
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (operation.type === 'insert_node') {
|
|
32
|
+
// Must be given a new key or adding/removing marks while typing gets in trouble (duped keys)!
|
|
33
|
+
const withNewKey = !isPreservingKeys(editor) || !('_key' in operation.node)
|
|
34
|
+
if (!Editor.isEditor(operation.node)) {
|
|
35
|
+
operation.node = {
|
|
36
|
+
...operation.node,
|
|
37
|
+
...(withNewKey ? {_key: keyGenerator()} : {}),
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
apply(operation)
|
|
42
|
+
}
|
|
43
|
+
editor.normalizeNode = (entry) => {
|
|
44
|
+
const [node, path] = entry
|
|
45
|
+
if (Element.isElement(node) && node._type === schemaTypes.block.name) {
|
|
46
|
+
// Set key on block itself
|
|
47
|
+
if (!node._key) {
|
|
48
|
+
Transforms.setNodes(editor, {_key: keyGenerator()}, {at: path})
|
|
49
|
+
}
|
|
50
|
+
// Set keys on it's children
|
|
51
|
+
for (const [child, childPath] of Node.children(editor, path)) {
|
|
52
|
+
if (!child._key) {
|
|
53
|
+
Transforms.setNodes(editor, {_key: keyGenerator()}, {at: childPath})
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
normalizeNode(entry)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return editor
|
|
62
|
+
}
|
|
63
|
+
}
|