@portabletext/editor 1.22.0 → 1.24.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/_chunks-cjs/behavior.core.cjs +65 -2
- package/lib/_chunks-cjs/behavior.core.cjs.map +1 -1
- package/lib/_chunks-cjs/util.slice-blocks.cjs +26 -12
- package/lib/_chunks-cjs/util.slice-blocks.cjs.map +1 -1
- package/lib/_chunks-es/behavior.core.js +65 -2
- package/lib/_chunks-es/behavior.core.js.map +1 -1
- package/lib/_chunks-es/util.slice-blocks.js +26 -12
- package/lib/_chunks-es/util.slice-blocks.js.map +1 -1
- package/lib/behaviors/index.d.cts +1111 -44
- package/lib/behaviors/index.d.ts +1111 -44
- package/lib/index.cjs +542 -333
- package/lib/index.cjs.map +1 -1
- package/lib/index.d.cts +446 -1
- package/lib/index.d.ts +446 -1
- package/lib/index.js +546 -335
- package/lib/index.js.map +1 -1
- package/lib/selectors/index.d.cts +73 -0
- package/lib/selectors/index.d.ts +73 -0
- package/package.json +23 -18
- package/src/behavior-actions/behavior.action.data-transfer-set.ts +7 -0
- package/src/behavior-actions/behavior.action.insert-blocks.ts +61 -0
- package/src/behavior-actions/behavior.actions.ts +75 -0
- package/src/behaviors/behavior.core.deserialize.ts +46 -0
- package/src/behaviors/behavior.core.serialize.ts +44 -0
- package/src/behaviors/behavior.core.ts +7 -0
- package/src/behaviors/behavior.types.ts +39 -2
- package/src/converters/converter.json.ts +53 -0
- package/src/converters/converter.portable-text.deserialize.test.ts +686 -0
- package/src/converters/converter.portable-text.ts +59 -0
- package/src/converters/converter.text-html.deserialize.test.ts +349 -0
- package/src/converters/converter.text-html.serialize.test.ts +233 -0
- package/src/converters/converter.text-html.ts +61 -0
- package/src/converters/converter.text-plain.test.ts +241 -0
- package/src/converters/converter.text-plain.ts +91 -0
- package/src/converters/converter.ts +65 -0
- package/src/converters/converters.ts +11 -0
- package/src/editor/Editable.tsx +3 -13
- package/src/editor/create-editor.ts +3 -0
- package/src/editor/editor-machine.ts +25 -1
- package/src/editor/editor-selector.ts +1 -0
- package/src/editor/editor-snapshot.ts +5 -0
- package/src/editor/plugins/create-with-event-listeners.ts +44 -0
- package/src/internal-utils/asserters.ts +9 -0
- package/src/internal-utils/mime-type.ts +1 -0
- package/src/internal-utils/parse-blocks.ts +136 -0
- package/src/internal-utils/test-key-generator.ts +9 -0
- package/src/selectors/selector.get-selected-spans.test.ts +1 -0
- package/src/selectors/selector.get-selection-text.test.ts +1 -0
- package/src/selectors/selector.is-active-decorator.test.ts +1 -0
- package/src/utils/util.slice-blocks.test.ts +216 -35
- package/src/utils/util.slice-blocks.ts +37 -10
- package/src/editor/plugins/__tests__/createWithInsertData.test.tsx +0 -181
- package/src/editor/plugins/createWithInsertData.ts +0 -425
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
import {isPortableTextSpan, isPortableTextTextBlock} from '@sanity/types'
|
|
2
|
-
import type {Descendant} from 'slate'
|
|
3
|
-
import {describe, expect, it} from 'vitest'
|
|
4
|
-
import {exportedForTesting} from '../createWithInsertData'
|
|
5
|
-
|
|
6
|
-
const initialValue = [
|
|
7
|
-
{
|
|
8
|
-
_key: 'a',
|
|
9
|
-
_type: 'myTestBlockType',
|
|
10
|
-
children: [
|
|
11
|
-
{
|
|
12
|
-
_key: 'a1',
|
|
13
|
-
_type: 'span',
|
|
14
|
-
marks: ['link1'],
|
|
15
|
-
text: 'Block A',
|
|
16
|
-
},
|
|
17
|
-
{
|
|
18
|
-
_key: 'a2',
|
|
19
|
-
_type: 'span',
|
|
20
|
-
marks: ['colour1'],
|
|
21
|
-
text: 'Block B',
|
|
22
|
-
},
|
|
23
|
-
],
|
|
24
|
-
markDefs: [
|
|
25
|
-
{
|
|
26
|
-
_key: 'link1',
|
|
27
|
-
_type: 'link',
|
|
28
|
-
href: 'google.com',
|
|
29
|
-
newTab: false,
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
_key: 'colour1',
|
|
33
|
-
_type: 'color',
|
|
34
|
-
color: 'red',
|
|
35
|
-
},
|
|
36
|
-
],
|
|
37
|
-
style: 'normal',
|
|
38
|
-
},
|
|
39
|
-
] satisfies Array<Descendant>
|
|
40
|
-
|
|
41
|
-
describe('plugin: createWithInsertData _regenerateKeys', () => {
|
|
42
|
-
it('has MarkDefs that are allowed annotations', async () => {
|
|
43
|
-
const {_regenerateKeys} = exportedForTesting
|
|
44
|
-
let keyCursor = 0
|
|
45
|
-
|
|
46
|
-
const generatedValue = _regenerateKeys(
|
|
47
|
-
{
|
|
48
|
-
isTextBlock: isPortableTextTextBlock,
|
|
49
|
-
isTextSpan: isPortableTextSpan,
|
|
50
|
-
},
|
|
51
|
-
initialValue,
|
|
52
|
-
() => {
|
|
53
|
-
keyCursor++
|
|
54
|
-
return `k${keyCursor}`
|
|
55
|
-
},
|
|
56
|
-
'span',
|
|
57
|
-
{
|
|
58
|
-
annotations: [
|
|
59
|
-
{
|
|
60
|
-
name: 'color',
|
|
61
|
-
jsonType: 'object',
|
|
62
|
-
fields: [],
|
|
63
|
-
__experimental_search: [],
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
name: 'link',
|
|
67
|
-
jsonType: 'object',
|
|
68
|
-
fields: [],
|
|
69
|
-
__experimental_search: [],
|
|
70
|
-
},
|
|
71
|
-
],
|
|
72
|
-
},
|
|
73
|
-
)
|
|
74
|
-
|
|
75
|
-
// the keys are not important here as it's not what we are testing here
|
|
76
|
-
expect(generatedValue).toStrictEqual([
|
|
77
|
-
{
|
|
78
|
-
_key: 'k3',
|
|
79
|
-
_type: 'myTestBlockType',
|
|
80
|
-
children: [
|
|
81
|
-
{_key: 'k4', _type: 'span', marks: ['k1'], text: 'Block A'},
|
|
82
|
-
{
|
|
83
|
-
_key: 'k5',
|
|
84
|
-
_type: 'span',
|
|
85
|
-
marks: ['k2'],
|
|
86
|
-
text: 'Block B',
|
|
87
|
-
},
|
|
88
|
-
],
|
|
89
|
-
markDefs: [
|
|
90
|
-
{_key: 'k1', _type: 'link', href: 'google.com', newTab: false},
|
|
91
|
-
{_key: 'k2', _type: 'color', color: 'red'},
|
|
92
|
-
],
|
|
93
|
-
style: 'normal',
|
|
94
|
-
},
|
|
95
|
-
])
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
it('removes MarkDefs when no annotations are allowed', async () => {
|
|
99
|
-
const {_regenerateKeys} = exportedForTesting
|
|
100
|
-
let keyCursor = 0
|
|
101
|
-
|
|
102
|
-
const generatedValue = _regenerateKeys(
|
|
103
|
-
{
|
|
104
|
-
isTextBlock: isPortableTextTextBlock,
|
|
105
|
-
isTextSpan: isPortableTextSpan,
|
|
106
|
-
},
|
|
107
|
-
initialValue,
|
|
108
|
-
() => {
|
|
109
|
-
keyCursor++
|
|
110
|
-
return `k${keyCursor}`
|
|
111
|
-
},
|
|
112
|
-
'span',
|
|
113
|
-
{annotations: []},
|
|
114
|
-
)
|
|
115
|
-
|
|
116
|
-
// orphaned children marks are removed later in the normalize function
|
|
117
|
-
expect(generatedValue).toStrictEqual([
|
|
118
|
-
{
|
|
119
|
-
_key: 'k1',
|
|
120
|
-
_type: 'myTestBlockType',
|
|
121
|
-
children: [
|
|
122
|
-
{_key: 'a1', _type: 'span', marks: ['link1'], text: 'Block A'},
|
|
123
|
-
{
|
|
124
|
-
_key: 'a2',
|
|
125
|
-
_type: 'span',
|
|
126
|
-
marks: ['colour1'],
|
|
127
|
-
text: 'Block B',
|
|
128
|
-
},
|
|
129
|
-
],
|
|
130
|
-
style: 'normal',
|
|
131
|
-
},
|
|
132
|
-
])
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
it('updates MarkDefs when one annotations is allowed but one is not allowed', async () => {
|
|
136
|
-
const {_regenerateKeys} = exportedForTesting
|
|
137
|
-
let keyCursor = 0
|
|
138
|
-
|
|
139
|
-
const generatedValue = _regenerateKeys(
|
|
140
|
-
{
|
|
141
|
-
isTextBlock: isPortableTextTextBlock,
|
|
142
|
-
isTextSpan: isPortableTextSpan,
|
|
143
|
-
},
|
|
144
|
-
initialValue,
|
|
145
|
-
() => {
|
|
146
|
-
keyCursor++
|
|
147
|
-
return `k${keyCursor}`
|
|
148
|
-
},
|
|
149
|
-
'span',
|
|
150
|
-
{
|
|
151
|
-
annotations: [
|
|
152
|
-
{
|
|
153
|
-
name: 'color',
|
|
154
|
-
jsonType: 'object',
|
|
155
|
-
fields: [],
|
|
156
|
-
__experimental_search: [],
|
|
157
|
-
},
|
|
158
|
-
],
|
|
159
|
-
},
|
|
160
|
-
)
|
|
161
|
-
|
|
162
|
-
// orphaned children marks are removed later in the normalize function
|
|
163
|
-
expect(generatedValue).toStrictEqual([
|
|
164
|
-
{
|
|
165
|
-
_key: 'k1',
|
|
166
|
-
_type: 'myTestBlockType',
|
|
167
|
-
children: [
|
|
168
|
-
{_key: 'a1', _type: 'span', marks: ['link1'], text: 'Block A'},
|
|
169
|
-
{
|
|
170
|
-
_key: 'a2',
|
|
171
|
-
_type: 'span',
|
|
172
|
-
marks: ['colour1'],
|
|
173
|
-
text: 'Block B',
|
|
174
|
-
},
|
|
175
|
-
],
|
|
176
|
-
markDefs: [{_key: 'colour1', _type: 'color', color: 'red'}],
|
|
177
|
-
style: 'normal',
|
|
178
|
-
},
|
|
179
|
-
])
|
|
180
|
-
})
|
|
181
|
-
})
|
|
@@ -1,425 +0,0 @@
|
|
|
1
|
-
import {htmlToBlocks} from '@portabletext/block-tools'
|
|
2
|
-
import type {PortableTextBlock, PortableTextChild} from '@sanity/types'
|
|
3
|
-
import {isEqual, uniq} from 'lodash'
|
|
4
|
-
import {Editor, Range, Transforms, type Descendant, type Node} from 'slate'
|
|
5
|
-
import {ReactEditor} from 'slate-react'
|
|
6
|
-
import {debugWithName} from '../../internal-utils/debug'
|
|
7
|
-
import {validateValue} from '../../internal-utils/validateValue'
|
|
8
|
-
import {
|
|
9
|
-
fromSlateValue,
|
|
10
|
-
isEqualToEmptyEditor,
|
|
11
|
-
toSlateValue,
|
|
12
|
-
} from '../../internal-utils/values'
|
|
13
|
-
import type {
|
|
14
|
-
PortableTextMemberSchemaTypes,
|
|
15
|
-
PortableTextSlateEditor,
|
|
16
|
-
} from '../../types/editor'
|
|
17
|
-
import type {EditorActor} from '../editor-machine'
|
|
18
|
-
|
|
19
|
-
const debug = debugWithName('plugin:withInsertData')
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* This plugin handles copy/paste in the editor
|
|
23
|
-
*
|
|
24
|
-
*/
|
|
25
|
-
export function createWithInsertData(
|
|
26
|
-
editorActor: EditorActor,
|
|
27
|
-
schemaTypes: PortableTextMemberSchemaTypes,
|
|
28
|
-
) {
|
|
29
|
-
return function withInsertData(
|
|
30
|
-
editor: PortableTextSlateEditor,
|
|
31
|
-
): PortableTextSlateEditor {
|
|
32
|
-
const blockTypeName = schemaTypes.block.name
|
|
33
|
-
const spanTypeName = schemaTypes.span.name
|
|
34
|
-
const whitespaceOnPasteMode =
|
|
35
|
-
schemaTypes.block.options.unstable_whitespaceOnPasteMode
|
|
36
|
-
|
|
37
|
-
const toPlainText = (blocks: PortableTextBlock[]) => {
|
|
38
|
-
return blocks
|
|
39
|
-
.map((block) => {
|
|
40
|
-
if (editor.isTextBlock(block)) {
|
|
41
|
-
return block.children
|
|
42
|
-
.map((child: PortableTextChild) => {
|
|
43
|
-
if (child._type === spanTypeName) {
|
|
44
|
-
return child.text
|
|
45
|
-
}
|
|
46
|
-
return `[${
|
|
47
|
-
schemaTypes.inlineObjects.find((t) => t.name === child._type)
|
|
48
|
-
?.title || 'Object'
|
|
49
|
-
}]`
|
|
50
|
-
})
|
|
51
|
-
.join('')
|
|
52
|
-
}
|
|
53
|
-
return `[${
|
|
54
|
-
schemaTypes.blockObjects.find((t) => t.name === block._type)
|
|
55
|
-
?.title || 'Object'
|
|
56
|
-
}]`
|
|
57
|
-
})
|
|
58
|
-
.join('\n\n')
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
editor.setFragmentData = (data: DataTransfer, originEvent) => {
|
|
62
|
-
const {selection} = editor
|
|
63
|
-
|
|
64
|
-
if (!selection) {
|
|
65
|
-
return
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const [start, end] = Range.edges(selection)
|
|
69
|
-
const startVoid = Editor.void(editor, {at: start.path})
|
|
70
|
-
const endVoid = Editor.void(editor, {at: end.path})
|
|
71
|
-
|
|
72
|
-
if (Range.isCollapsed(selection) && !startVoid) {
|
|
73
|
-
return
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Create a fake selection so that we can add a Base64-encoded copy of the
|
|
77
|
-
// fragment to the HTML, to decode on future pastes.
|
|
78
|
-
const domRange = ReactEditor.toDOMRange(editor, selection)
|
|
79
|
-
let contents = domRange.cloneContents()
|
|
80
|
-
// COMPAT: If the end node is a void node, we need to move the end of the
|
|
81
|
-
// range from the void node's spacer span, to the end of the void node's
|
|
82
|
-
// content, since the spacer is before void's content in the DOM.
|
|
83
|
-
if (endVoid) {
|
|
84
|
-
const [voidNode] = endVoid
|
|
85
|
-
const r = domRange.cloneRange()
|
|
86
|
-
const domNode = ReactEditor.toDOMNode(editor, voidNode)
|
|
87
|
-
r.setEndAfter(domNode)
|
|
88
|
-
contents = r.cloneContents()
|
|
89
|
-
}
|
|
90
|
-
// Remove any zero-width space spans from the cloned DOM so that they don't
|
|
91
|
-
// show up elsewhere when pasted.
|
|
92
|
-
Array.from(contents.querySelectorAll('[data-slate-zero-width]')).forEach(
|
|
93
|
-
(zw) => {
|
|
94
|
-
const isNewline = zw.getAttribute('data-slate-zero-width') === 'n'
|
|
95
|
-
zw.textContent = isNewline ? '\n' : ''
|
|
96
|
-
},
|
|
97
|
-
)
|
|
98
|
-
// Clean up the clipboard HTML for editor spesific attributes
|
|
99
|
-
Array.from(contents.querySelectorAll('*')).forEach((elm) => {
|
|
100
|
-
elm.removeAttribute('contentEditable')
|
|
101
|
-
elm.removeAttribute('data-slate-inline')
|
|
102
|
-
elm.removeAttribute('data-slate-leaf')
|
|
103
|
-
elm.removeAttribute('data-slate-node')
|
|
104
|
-
elm.removeAttribute('data-slate-spacer')
|
|
105
|
-
elm.removeAttribute('data-slate-string')
|
|
106
|
-
elm.removeAttribute('data-slate-zero-width')
|
|
107
|
-
elm.removeAttribute('draggable')
|
|
108
|
-
for (const key in elm.attributes) {
|
|
109
|
-
if (elm.hasAttribute(key)) {
|
|
110
|
-
elm.removeAttribute(key)
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
})
|
|
114
|
-
const div = contents.ownerDocument.createElement('div')
|
|
115
|
-
div.appendChild(contents)
|
|
116
|
-
div.setAttribute('hidden', 'true')
|
|
117
|
-
contents.ownerDocument.body.appendChild(div)
|
|
118
|
-
const asHTML = div.innerHTML
|
|
119
|
-
contents.ownerDocument.body.removeChild(div)
|
|
120
|
-
const fragment = editor.getFragment()
|
|
121
|
-
const portableText = fromSlateValue(fragment, blockTypeName)
|
|
122
|
-
|
|
123
|
-
const asJSON = JSON.stringify(portableText)
|
|
124
|
-
const asPlainText = toPlainText(portableText)
|
|
125
|
-
data.clearData()
|
|
126
|
-
data.setData('text/plain', asPlainText)
|
|
127
|
-
data.setData('text/html', asHTML)
|
|
128
|
-
data.setData('application/json', asJSON)
|
|
129
|
-
data.setData('application/x-portable-text', asJSON)
|
|
130
|
-
debug('text', asPlainText)
|
|
131
|
-
data.setData(
|
|
132
|
-
'application/x-portable-text-event-origin',
|
|
133
|
-
originEvent || 'external',
|
|
134
|
-
)
|
|
135
|
-
debug('Set fragment data', asJSON, asHTML)
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
editor.insertPortableTextData = (data: DataTransfer): boolean => {
|
|
139
|
-
if (!editor.selection) {
|
|
140
|
-
return false
|
|
141
|
-
}
|
|
142
|
-
const pText = data.getData('application/x-portable-text')
|
|
143
|
-
const origin = data.getData('application/x-portable-text-event-origin')
|
|
144
|
-
debug(`Inserting portable text from ${origin} event`, pText)
|
|
145
|
-
if (pText) {
|
|
146
|
-
const parsed = JSON.parse(pText) as PortableTextBlock[]
|
|
147
|
-
if (Array.isArray(parsed) && parsed.length > 0) {
|
|
148
|
-
const slateValue = _regenerateKeys(
|
|
149
|
-
editor,
|
|
150
|
-
toSlateValue(parsed, {schemaTypes}),
|
|
151
|
-
editorActor.getSnapshot().context.keyGenerator,
|
|
152
|
-
spanTypeName,
|
|
153
|
-
schemaTypes,
|
|
154
|
-
)
|
|
155
|
-
// Validate the result
|
|
156
|
-
const validation = validateValue(
|
|
157
|
-
parsed,
|
|
158
|
-
schemaTypes,
|
|
159
|
-
editorActor.getSnapshot().context.keyGenerator,
|
|
160
|
-
)
|
|
161
|
-
// Bail out if it's not valid
|
|
162
|
-
if (!validation.valid && !validation.resolution?.autoResolve) {
|
|
163
|
-
const errorDescription = `${validation.resolution?.description}`
|
|
164
|
-
editorActor.send({
|
|
165
|
-
type: 'error',
|
|
166
|
-
name: 'pasteError',
|
|
167
|
-
description: errorDescription,
|
|
168
|
-
data: validation,
|
|
169
|
-
})
|
|
170
|
-
debug('Invalid insert result', validation)
|
|
171
|
-
return false
|
|
172
|
-
}
|
|
173
|
-
_insertFragment(editor, slateValue, schemaTypes)
|
|
174
|
-
return true
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
return false
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
editor.insertTextOrHTMLData = (data: DataTransfer): boolean => {
|
|
181
|
-
if (!editor.selection) {
|
|
182
|
-
debug('No selection, not inserting')
|
|
183
|
-
return false
|
|
184
|
-
}
|
|
185
|
-
const html = data.getData('text/html')
|
|
186
|
-
const text = data.getData('text/plain')
|
|
187
|
-
|
|
188
|
-
if (html || text) {
|
|
189
|
-
debug('Inserting data', data)
|
|
190
|
-
let portableText: PortableTextBlock[]
|
|
191
|
-
let fragment: Node[]
|
|
192
|
-
let insertedType: string | undefined
|
|
193
|
-
|
|
194
|
-
if (html) {
|
|
195
|
-
portableText = htmlToBlocks(html, schemaTypes.portableText, {
|
|
196
|
-
unstable_whitespaceOnPasteMode: whitespaceOnPasteMode,
|
|
197
|
-
keyGenerator: editorActor.getSnapshot().context.keyGenerator,
|
|
198
|
-
}) as PortableTextBlock[]
|
|
199
|
-
fragment = toSlateValue(portableText, {schemaTypes})
|
|
200
|
-
insertedType = 'HTML'
|
|
201
|
-
|
|
202
|
-
if (portableText.length === 0) {
|
|
203
|
-
return false
|
|
204
|
-
}
|
|
205
|
-
} else {
|
|
206
|
-
// plain text
|
|
207
|
-
const blocks = escapeHtml(text)
|
|
208
|
-
.split(/\n{2,}/)
|
|
209
|
-
.map((line) =>
|
|
210
|
-
line
|
|
211
|
-
? `<p>${line.replace(/(?:\r\n|\r|\n)/g, '<br/>')}</p>`
|
|
212
|
-
: '<p></p>',
|
|
213
|
-
)
|
|
214
|
-
.join('')
|
|
215
|
-
const textToHtml = `<html><body>${blocks}</body></html>`
|
|
216
|
-
portableText = htmlToBlocks(textToHtml, schemaTypes.portableText, {
|
|
217
|
-
keyGenerator: editorActor.getSnapshot().context.keyGenerator,
|
|
218
|
-
}) as PortableTextBlock[]
|
|
219
|
-
fragment = toSlateValue(portableText, {
|
|
220
|
-
schemaTypes,
|
|
221
|
-
})
|
|
222
|
-
insertedType = 'text'
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Validate the result
|
|
226
|
-
const validation = validateValue(
|
|
227
|
-
portableText,
|
|
228
|
-
schemaTypes,
|
|
229
|
-
editorActor.getSnapshot().context.keyGenerator,
|
|
230
|
-
)
|
|
231
|
-
|
|
232
|
-
// Bail out if it's not valid
|
|
233
|
-
if (!validation.valid) {
|
|
234
|
-
const errorDescription = `Could not validate the resulting portable text to insert.\n${validation.resolution?.description}\nTry to insert as plain text (shift-paste) instead.`
|
|
235
|
-
editorActor.send({
|
|
236
|
-
type: 'error',
|
|
237
|
-
name: 'pasteError',
|
|
238
|
-
description: errorDescription,
|
|
239
|
-
data: validation,
|
|
240
|
-
})
|
|
241
|
-
debug('Invalid insert result', validation)
|
|
242
|
-
return false
|
|
243
|
-
}
|
|
244
|
-
debug(
|
|
245
|
-
`Inserting ${insertedType} fragment at ${JSON.stringify(editor.selection)}`,
|
|
246
|
-
)
|
|
247
|
-
_insertFragment(editor, fragment, schemaTypes)
|
|
248
|
-
return true
|
|
249
|
-
}
|
|
250
|
-
return false
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
editor.insertData = (data: DataTransfer) => {
|
|
254
|
-
if (!editor.insertPortableTextData(data)) {
|
|
255
|
-
editor.insertTextOrHTMLData(data)
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
editor.insertFragmentData = (data: DataTransfer): boolean => {
|
|
260
|
-
const fragment = data.getData('application/x-portable-text')
|
|
261
|
-
if (fragment) {
|
|
262
|
-
const parsed = JSON.parse(fragment)
|
|
263
|
-
editor.insertFragment(parsed)
|
|
264
|
-
return true
|
|
265
|
-
}
|
|
266
|
-
return false
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
return editor
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
const entityMap: Record<string, string> = {
|
|
274
|
-
'&': '&',
|
|
275
|
-
'<': '<',
|
|
276
|
-
'>': '>',
|
|
277
|
-
'"': '"',
|
|
278
|
-
"'": ''',
|
|
279
|
-
'/': '/',
|
|
280
|
-
'`': '`',
|
|
281
|
-
'=': '=',
|
|
282
|
-
}
|
|
283
|
-
function escapeHtml(str: string) {
|
|
284
|
-
return String(str).replace(/[&<>"'`=/]/g, (s: string) => entityMap[s])
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* Shared helper function to regenerate the keys on a fragment.
|
|
289
|
-
*
|
|
290
|
-
* @internal
|
|
291
|
-
*/
|
|
292
|
-
function _regenerateKeys(
|
|
293
|
-
editor: Pick<PortableTextSlateEditor, 'isTextBlock' | 'isTextSpan'>,
|
|
294
|
-
fragment: Descendant[],
|
|
295
|
-
keyGenerator: () => string,
|
|
296
|
-
spanTypeName: string,
|
|
297
|
-
editorTypes: Pick<PortableTextMemberSchemaTypes, 'annotations'>,
|
|
298
|
-
): Descendant[] {
|
|
299
|
-
return fragment.map((node) => {
|
|
300
|
-
const newNode: Descendant = {...node}
|
|
301
|
-
// Ensure the copy has new keys
|
|
302
|
-
if (editor.isTextBlock(newNode)) {
|
|
303
|
-
const annotations = editorTypes.annotations.map((t) => t.name)
|
|
304
|
-
|
|
305
|
-
// Ensure that if there are no annotations, we remove the markDefs
|
|
306
|
-
if (annotations.length === 0) {
|
|
307
|
-
const {markDefs, ...NewNodeNoDefs} = newNode
|
|
308
|
-
|
|
309
|
-
return {...NewNodeNoDefs, _key: keyGenerator()}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// Ensure that all annotations are allowed
|
|
313
|
-
const hasForbiddenAnnotations = (newNode.markDefs || []).some((def) => {
|
|
314
|
-
return !annotations.includes(def._type)
|
|
315
|
-
})
|
|
316
|
-
|
|
317
|
-
// if they have forbidden annotations, we remove them and keep the rest
|
|
318
|
-
if (hasForbiddenAnnotations) {
|
|
319
|
-
const allowedAnnotations = (newNode.markDefs || []).filter((def) => {
|
|
320
|
-
return annotations.includes(def._type)
|
|
321
|
-
})
|
|
322
|
-
|
|
323
|
-
return {...newNode, markDefs: allowedAnnotations, _key: keyGenerator()}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
newNode.markDefs = (newNode.markDefs || []).map((def) => {
|
|
327
|
-
const oldKey = def._key
|
|
328
|
-
const newKey = keyGenerator()
|
|
329
|
-
newNode.children = newNode.children.map((child) =>
|
|
330
|
-
child._type === spanTypeName && editor.isTextSpan(child)
|
|
331
|
-
? {
|
|
332
|
-
...child,
|
|
333
|
-
marks:
|
|
334
|
-
child.marks && child.marks.includes(oldKey)
|
|
335
|
-
? [...child.marks]
|
|
336
|
-
.filter((mark) => mark !== oldKey)
|
|
337
|
-
.concat(newKey)
|
|
338
|
-
: child.marks,
|
|
339
|
-
}
|
|
340
|
-
: child,
|
|
341
|
-
)
|
|
342
|
-
return {...def, _key: newKey}
|
|
343
|
-
})
|
|
344
|
-
}
|
|
345
|
-
const nodeWithNewKeys = {...newNode, _key: keyGenerator()}
|
|
346
|
-
if (editor.isTextBlock(nodeWithNewKeys)) {
|
|
347
|
-
nodeWithNewKeys.children = nodeWithNewKeys.children.map((child) => ({
|
|
348
|
-
...child,
|
|
349
|
-
_key: keyGenerator(),
|
|
350
|
-
}))
|
|
351
|
-
}
|
|
352
|
-
return nodeWithNewKeys as Descendant
|
|
353
|
-
})
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
/**
|
|
357
|
-
* Shared helper function to insert the final fragment into the editor
|
|
358
|
-
*
|
|
359
|
-
* @internal
|
|
360
|
-
*/
|
|
361
|
-
function _insertFragment(
|
|
362
|
-
editor: PortableTextSlateEditor,
|
|
363
|
-
fragment: Descendant[],
|
|
364
|
-
schemaTypes: PortableTextMemberSchemaTypes,
|
|
365
|
-
) {
|
|
366
|
-
editor.withoutNormalizing(() => {
|
|
367
|
-
if (!editor.selection) {
|
|
368
|
-
return
|
|
369
|
-
}
|
|
370
|
-
// Ensure that markDefs for any annotations inside this fragment are copied over to the focused text block.
|
|
371
|
-
const [focusBlock, focusPath] = Editor.node(editor, editor.selection, {
|
|
372
|
-
depth: 1,
|
|
373
|
-
})
|
|
374
|
-
if (editor.isTextBlock(focusBlock) && editor.isTextBlock(fragment[0])) {
|
|
375
|
-
const {markDefs} = focusBlock
|
|
376
|
-
debug(
|
|
377
|
-
'Mixing markDefs of focusBlock and fragments[0] block',
|
|
378
|
-
markDefs,
|
|
379
|
-
fragment[0].markDefs,
|
|
380
|
-
)
|
|
381
|
-
if (!isEqual(markDefs, fragment[0].markDefs)) {
|
|
382
|
-
Transforms.setNodes(
|
|
383
|
-
editor,
|
|
384
|
-
{
|
|
385
|
-
markDefs: uniq([
|
|
386
|
-
...(fragment[0].markDefs || []),
|
|
387
|
-
...(markDefs || []),
|
|
388
|
-
]),
|
|
389
|
-
},
|
|
390
|
-
{at: focusPath, mode: 'lowest', voids: false},
|
|
391
|
-
)
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
const isPasteToEmptyEditor = isEqualToEmptyEditor(
|
|
396
|
-
editor.children,
|
|
397
|
-
schemaTypes,
|
|
398
|
-
)
|
|
399
|
-
|
|
400
|
-
if (isPasteToEmptyEditor) {
|
|
401
|
-
// Special case for pasting directly into an empty editor (a placeholder block).
|
|
402
|
-
// When pasting content starting with multiple empty blocks,
|
|
403
|
-
// `editor.insertFragment` can potentially duplicate the keys of
|
|
404
|
-
// the placeholder block because of operations that happen
|
|
405
|
-
// inside `editor.insertFragment` (involves an `insert_node` operation).
|
|
406
|
-
// However by splitting the placeholder block first in this situation we are good.
|
|
407
|
-
Transforms.splitNodes(editor, {at: [0, 0]})
|
|
408
|
-
editor.insertFragment(fragment)
|
|
409
|
-
Transforms.removeNodes(editor, {at: [0]})
|
|
410
|
-
} else {
|
|
411
|
-
// All other inserts
|
|
412
|
-
editor.insertFragment(fragment)
|
|
413
|
-
}
|
|
414
|
-
})
|
|
415
|
-
|
|
416
|
-
editor.onChange()
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
/**
|
|
420
|
-
* functions we don't want to export but want to test
|
|
421
|
-
* @internal
|
|
422
|
-
*/
|
|
423
|
-
export const exportedForTesting = {
|
|
424
|
-
_regenerateKeys,
|
|
425
|
-
}
|