@portabletext/editor 3.2.6 → 3.3.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/_chunks-es/use-editor.js +8 -4
- package/lib/_chunks-es/use-editor.js.map +1 -1
- package/lib/index.js +130 -67
- package/lib/index.js.map +1 -1
- package/package.json +2 -1
- package/src/behaviors/behavior.abstract.deserialize.ts +68 -84
- package/src/converters/converter.text-markdown.ts +67 -0
- package/src/converters/converters.core.ts +2 -0
- package/src/editor/PortableTextEditor.tsx +3 -1
- package/src/editor/plugins/createWithPatches.ts +4 -2
- package/src/editor/sync-machine.ts +8 -3
- package/src/internal-utils/apply-operation-to-portable-text.ts +3 -1
- package/src/internal-utils/event-position.ts +43 -4
- package/src/internal-utils/global-scope.ts +12 -4
- package/src/priority/priority.sort.ts +3 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@portabletext/editor",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.1",
|
|
4
4
|
"description": "Portable Text Editor made in React",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sanity",
|
|
@@ -73,6 +73,7 @@
|
|
|
73
73
|
"slate-react": "^0.120.0",
|
|
74
74
|
"xstate": "^5.24.0",
|
|
75
75
|
"@portabletext/block-tools": "^4.1.7",
|
|
76
|
+
"@portabletext/markdown": "^1.0.3",
|
|
76
77
|
"@portabletext/keyboard-shortcuts": "^2.1.0",
|
|
77
78
|
"@portabletext/patches": "^2.0.0",
|
|
78
79
|
"@portabletext/schema": "^2.0.0"
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {isTextBlock} from '@portabletext/schema'
|
|
2
|
+
import type {MIMEType} from '../internal-utils/mime-type'
|
|
2
3
|
import {getActiveAnnotations} from '../selectors/selector.get-active-annotations'
|
|
3
4
|
import {getActiveDecorators} from '../selectors/selector.get-active-decorators'
|
|
4
5
|
import {getFocusTextBlock} from '../selectors/selector.get-focus-text-block'
|
|
@@ -6,60 +7,67 @@ import {getTextBlockText} from '../utils/util.get-text-block-text'
|
|
|
6
7
|
import {raise} from './behavior.types.action'
|
|
7
8
|
import {defineBehavior} from './behavior.types.behavior'
|
|
8
9
|
|
|
10
|
+
const mimeTypePriority: Array<MIMEType> = [
|
|
11
|
+
'application/x-portable-text',
|
|
12
|
+
'application/json',
|
|
13
|
+
'text/markdown',
|
|
14
|
+
'text/html',
|
|
15
|
+
'text/plain',
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Finds the first available data from the dataTransfer based on priority.
|
|
20
|
+
* Optionally starts from a specific mime type in the priority list.
|
|
21
|
+
*/
|
|
22
|
+
function getFirstAvailableData({
|
|
23
|
+
dataTransfer,
|
|
24
|
+
startAfter,
|
|
25
|
+
}: {
|
|
26
|
+
dataTransfer: DataTransfer
|
|
27
|
+
startAfter?: MIMEType
|
|
28
|
+
}): {mimeType: MIMEType; data: string} | undefined {
|
|
29
|
+
const startIndex = startAfter ? mimeTypePriority.indexOf(startAfter) + 1 : 0
|
|
30
|
+
|
|
31
|
+
for (let index = startIndex; index < mimeTypePriority.length; index++) {
|
|
32
|
+
const mimeType = mimeTypePriority.at(index)
|
|
33
|
+
|
|
34
|
+
if (!mimeType) {
|
|
35
|
+
continue
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const data = dataTransfer.getData(mimeType)
|
|
39
|
+
|
|
40
|
+
if (!data) {
|
|
41
|
+
continue
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
mimeType,
|
|
46
|
+
data,
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return undefined
|
|
51
|
+
}
|
|
52
|
+
|
|
9
53
|
export const abstractDeserializeBehaviors = [
|
|
10
54
|
defineBehavior({
|
|
11
55
|
on: 'deserialize',
|
|
12
56
|
guard: ({event}) => {
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
)
|
|
16
|
-
|
|
17
|
-
if (portableText) {
|
|
18
|
-
return {
|
|
19
|
-
type: 'deserialize.data',
|
|
20
|
-
mimeType: 'application/x-portable-text',
|
|
21
|
-
data: portableText,
|
|
22
|
-
originEvent: event.originEvent,
|
|
23
|
-
} as const
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const json =
|
|
27
|
-
event.originEvent.originEvent.dataTransfer.getData('application/json')
|
|
28
|
-
|
|
29
|
-
if (json) {
|
|
30
|
-
return {
|
|
31
|
-
type: 'deserialize.data',
|
|
32
|
-
mimeType: 'application/json',
|
|
33
|
-
data: json,
|
|
34
|
-
originEvent: event.originEvent,
|
|
35
|
-
} as const
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const html =
|
|
39
|
-
event.originEvent.originEvent.dataTransfer.getData('text/html')
|
|
57
|
+
const availableData = getFirstAvailableData({
|
|
58
|
+
dataTransfer: event.originEvent.originEvent.dataTransfer,
|
|
59
|
+
})
|
|
40
60
|
|
|
41
|
-
if (
|
|
42
|
-
return
|
|
43
|
-
type: 'deserialize.data',
|
|
44
|
-
mimeType: 'text/html',
|
|
45
|
-
data: html,
|
|
46
|
-
originEvent: event.originEvent,
|
|
47
|
-
} as const
|
|
61
|
+
if (!availableData) {
|
|
62
|
+
return false
|
|
48
63
|
}
|
|
49
64
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
type: 'deserialize.data',
|
|
56
|
-
mimeType: 'text/plain',
|
|
57
|
-
data: text,
|
|
58
|
-
originEvent: event.originEvent,
|
|
59
|
-
} as const
|
|
65
|
+
return {
|
|
66
|
+
type: 'deserialize.data' as const,
|
|
67
|
+
mimeType: availableData.mimeType,
|
|
68
|
+
data: availableData.data,
|
|
69
|
+
originEvent: event.originEvent,
|
|
60
70
|
}
|
|
61
|
-
|
|
62
|
-
return false
|
|
63
71
|
},
|
|
64
72
|
actions: [(_, deserializeEvent) => [raise(deserializeEvent)]],
|
|
65
73
|
}),
|
|
@@ -184,49 +192,25 @@ export const abstractDeserializeBehaviors = [
|
|
|
184
192
|
defineBehavior({
|
|
185
193
|
on: 'deserialization.failure',
|
|
186
194
|
guard: ({event}) => {
|
|
187
|
-
if (event.mimeType === '
|
|
188
|
-
|
|
189
|
-
event.originEvent.originEvent.dataTransfer.getData('application/json')
|
|
190
|
-
|
|
191
|
-
if (json) {
|
|
192
|
-
return {
|
|
193
|
-
type: 'deserialize.data',
|
|
194
|
-
mimeType: 'application/json',
|
|
195
|
-
data: json,
|
|
196
|
-
originEvent: event.originEvent,
|
|
197
|
-
} as const
|
|
198
|
-
}
|
|
195
|
+
if (event.mimeType === '*/*') {
|
|
196
|
+
return false
|
|
199
197
|
}
|
|
200
198
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
if (html) {
|
|
206
|
-
return {
|
|
207
|
-
type: 'deserialize.data',
|
|
208
|
-
mimeType: 'text/html',
|
|
209
|
-
data: html,
|
|
210
|
-
originEvent: event.originEvent,
|
|
211
|
-
} as const
|
|
212
|
-
}
|
|
213
|
-
}
|
|
199
|
+
const availableData = getFirstAvailableData({
|
|
200
|
+
dataTransfer: event.originEvent.originEvent.dataTransfer,
|
|
201
|
+
startAfter: event.mimeType,
|
|
202
|
+
})
|
|
214
203
|
|
|
215
|
-
if (
|
|
216
|
-
|
|
217
|
-
event.originEvent.originEvent.dataTransfer.getData('text/plain')
|
|
218
|
-
|
|
219
|
-
if (text) {
|
|
220
|
-
return {
|
|
221
|
-
type: 'deserialize.data',
|
|
222
|
-
mimeType: 'text/plain',
|
|
223
|
-
data: text,
|
|
224
|
-
originEvent: event.originEvent,
|
|
225
|
-
} as const
|
|
226
|
-
}
|
|
204
|
+
if (!availableData) {
|
|
205
|
+
return false
|
|
227
206
|
}
|
|
228
207
|
|
|
229
|
-
return
|
|
208
|
+
return {
|
|
209
|
+
type: 'deserialize.data' as const,
|
|
210
|
+
mimeType: availableData.mimeType,
|
|
211
|
+
data: availableData.data,
|
|
212
|
+
originEvent: event.originEvent,
|
|
213
|
+
}
|
|
230
214
|
},
|
|
231
215
|
actions: [(_, deserializeDataEvent) => [raise(deserializeDataEvent)]],
|
|
232
216
|
}),
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import {
|
|
2
|
+
markdownToPortableText,
|
|
3
|
+
portableTextToMarkdown,
|
|
4
|
+
} from '@portabletext/markdown'
|
|
5
|
+
import {getSelectedValue} from '../selectors/selector.get-selected-value'
|
|
6
|
+
import {parseBlock} from '../utils/parse-blocks'
|
|
7
|
+
import {defineConverter} from './converter.types'
|
|
8
|
+
|
|
9
|
+
export const converterTextMarkdown = defineConverter({
|
|
10
|
+
mimeType: 'text/markdown',
|
|
11
|
+
serialize: ({snapshot, event}) => {
|
|
12
|
+
const selection = snapshot.context.selection
|
|
13
|
+
|
|
14
|
+
if (!selection) {
|
|
15
|
+
return {
|
|
16
|
+
type: 'serialization.failure',
|
|
17
|
+
mimeType: 'text/markdown',
|
|
18
|
+
reason: 'No selection',
|
|
19
|
+
originEvent: event.originEvent,
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const blocks = getSelectedValue(snapshot)
|
|
24
|
+
|
|
25
|
+
const markdown = portableTextToMarkdown(blocks)
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
type: 'serialization.success',
|
|
29
|
+
data: markdown,
|
|
30
|
+
mimeType: 'text/markdown',
|
|
31
|
+
originEvent: event.originEvent,
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
deserialize: ({snapshot, event}) => {
|
|
35
|
+
const blocks = markdownToPortableText(event.data, {
|
|
36
|
+
keyGenerator: snapshot.context.keyGenerator,
|
|
37
|
+
schema: snapshot.context.schema,
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const parsedBlocks = blocks.flatMap((block) => {
|
|
41
|
+
const parsedBlock = parseBlock({
|
|
42
|
+
context: snapshot.context,
|
|
43
|
+
block,
|
|
44
|
+
options: {
|
|
45
|
+
normalize: false,
|
|
46
|
+
removeUnusedMarkDefs: true,
|
|
47
|
+
validateFields: false,
|
|
48
|
+
},
|
|
49
|
+
})
|
|
50
|
+
return parsedBlock ? [parsedBlock] : []
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
if (parsedBlocks.length === 0) {
|
|
54
|
+
return {
|
|
55
|
+
type: 'deserialization.failure',
|
|
56
|
+
mimeType: 'text/markdown',
|
|
57
|
+
reason: 'No blocks deserialized',
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
type: 'deserialization.success',
|
|
63
|
+
data: parsedBlocks,
|
|
64
|
+
mimeType: 'text/markdown',
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
})
|
|
@@ -2,6 +2,7 @@ import type {PortableTextMemberSchemaTypes} from '../types/editor'
|
|
|
2
2
|
import {converterJson} from './converter.json'
|
|
3
3
|
import {converterPortableText} from './converter.portable-text'
|
|
4
4
|
import {createConverterTextHtml} from './converter.text-html'
|
|
5
|
+
import {converterTextMarkdown} from './converter.text-markdown'
|
|
5
6
|
import {createConverterTextPlain} from './converter.text-plain'
|
|
6
7
|
|
|
7
8
|
export function createCoreConverters(
|
|
@@ -10,6 +11,7 @@ export function createCoreConverters(
|
|
|
10
11
|
return [
|
|
11
12
|
converterJson,
|
|
12
13
|
converterPortableText,
|
|
14
|
+
converterTextMarkdown,
|
|
13
15
|
createConverterTextHtml(legacySchema),
|
|
14
16
|
createConverterTextPlain(legacySchema),
|
|
15
17
|
]
|
|
@@ -599,7 +599,9 @@ export class PortableTextEditor extends Component<
|
|
|
599
599
|
}
|
|
600
600
|
|
|
601
601
|
static isObjectPath = (_editor: PortableTextEditor, path: Path): boolean => {
|
|
602
|
-
if (!path || !Array.isArray(path))
|
|
602
|
+
if (!path || !Array.isArray(path)) {
|
|
603
|
+
return false
|
|
604
|
+
}
|
|
603
605
|
const isChildObjectEditPath = path.length > 3 && path[1] === 'children'
|
|
604
606
|
const isBlockObjectEditPath = path.length > 1 && path[1] !== 'children'
|
|
605
607
|
return isBlockObjectEditPath || isChildObjectEditPath
|
|
@@ -64,8 +64,9 @@ export function createWithPatches({
|
|
|
64
64
|
withoutPatching(editor, () => {
|
|
65
65
|
pluginWithoutHistory(editor, () => {
|
|
66
66
|
for (const patch of patches) {
|
|
67
|
-
if (debug.enabled)
|
|
67
|
+
if (debug.enabled) {
|
|
68
68
|
debug(`Handling remote patch ${JSON.stringify(patch)}`)
|
|
69
|
+
}
|
|
69
70
|
|
|
70
71
|
try {
|
|
71
72
|
changed = applyPatch(editor, patch)
|
|
@@ -125,11 +126,12 @@ export function createWithPatches({
|
|
|
125
126
|
)
|
|
126
127
|
|
|
127
128
|
if (!isPatching(editor)) {
|
|
128
|
-
if (debugVerbose && debug.enabled)
|
|
129
|
+
if (debugVerbose && debug.enabled) {
|
|
129
130
|
debug(
|
|
130
131
|
`Editor is not producing patch for operation ${operation.type}`,
|
|
131
132
|
operation,
|
|
132
133
|
)
|
|
134
|
+
}
|
|
133
135
|
return editor
|
|
134
136
|
}
|
|
135
137
|
|
|
@@ -663,8 +663,9 @@ function syncBlock({
|
|
|
663
663
|
context.keyGenerator,
|
|
664
664
|
)
|
|
665
665
|
|
|
666
|
-
if (debug.enabled)
|
|
666
|
+
if (debug.enabled) {
|
|
667
667
|
debug('Validating and inserting new block in the end of the value', block)
|
|
668
|
+
}
|
|
668
669
|
|
|
669
670
|
if (validation.valid || validation.resolution?.autoResolve) {
|
|
670
671
|
const slateBlock = toSlateBlock(block, {
|
|
@@ -740,7 +741,9 @@ function syncBlock({
|
|
|
740
741
|
|
|
741
742
|
if (validation.valid || validation.resolution?.autoResolve) {
|
|
742
743
|
if (oldBlock._key === block._key) {
|
|
743
|
-
if (debug.enabled)
|
|
744
|
+
if (debug.enabled) {
|
|
745
|
+
debug('Updating block', oldBlock, block)
|
|
746
|
+
}
|
|
744
747
|
|
|
745
748
|
Editor.withoutNormalizing(slateEditor, () => {
|
|
746
749
|
withRemoteChanges(slateEditor, () => {
|
|
@@ -756,7 +759,9 @@ function syncBlock({
|
|
|
756
759
|
})
|
|
757
760
|
})
|
|
758
761
|
} else {
|
|
759
|
-
if (debug.enabled)
|
|
762
|
+
if (debug.enabled) {
|
|
763
|
+
debug('Replacing block', oldBlock, block)
|
|
764
|
+
}
|
|
760
765
|
|
|
761
766
|
Editor.withoutNormalizing(slateEditor, () => {
|
|
762
767
|
withRemoteChanges(slateEditor, () => {
|
|
@@ -145,7 +145,9 @@ function applyOperationToPortableTextImmutable(
|
|
|
145
145
|
|
|
146
146
|
case 'insert_text': {
|
|
147
147
|
const {path, offset, text} = operation
|
|
148
|
-
if (text.length === 0)
|
|
148
|
+
if (text.length === 0) {
|
|
149
|
+
return root
|
|
150
|
+
}
|
|
149
151
|
|
|
150
152
|
const span = getSpan(context, root, path)
|
|
151
153
|
if (!span) {
|
|
@@ -148,7 +148,13 @@ export function getEventNode({
|
|
|
148
148
|
return undefined
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
|
|
151
|
+
let node: Node | undefined
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
node = DOMEditor.toSlateNode(slateEditor, event.target)
|
|
155
|
+
} catch (error) {
|
|
156
|
+
console.error(error)
|
|
157
|
+
}
|
|
152
158
|
|
|
153
159
|
return node
|
|
154
160
|
}
|
|
@@ -168,7 +174,18 @@ function getEventPositionBlock({
|
|
|
168
174
|
return undefined
|
|
169
175
|
}
|
|
170
176
|
|
|
171
|
-
|
|
177
|
+
let firstBlockElement: HTMLElement | undefined
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
firstBlockElement = DOMEditor.toDOMNode(slateEditor, firstBlock)
|
|
181
|
+
} catch (error) {
|
|
182
|
+
console.error(error)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (!firstBlockElement) {
|
|
186
|
+
return undefined
|
|
187
|
+
}
|
|
188
|
+
|
|
172
189
|
const firstBlockRect = firstBlockElement.getBoundingClientRect()
|
|
173
190
|
|
|
174
191
|
if (event.pageY < firstBlockRect.top) {
|
|
@@ -181,14 +198,36 @@ function getEventPositionBlock({
|
|
|
181
198
|
return undefined
|
|
182
199
|
}
|
|
183
200
|
|
|
184
|
-
|
|
201
|
+
let lastBlockElement: HTMLElement | undefined
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
lastBlockElement = DOMEditor.toDOMNode(slateEditor, lastBlock)
|
|
205
|
+
} catch (error) {
|
|
206
|
+
console.error(error)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (!lastBlockElement) {
|
|
210
|
+
return undefined
|
|
211
|
+
}
|
|
212
|
+
|
|
185
213
|
const lastBlockRef = lastBlockElement.getBoundingClientRect()
|
|
186
214
|
|
|
187
215
|
if (event.pageY > lastBlockRef.bottom) {
|
|
188
216
|
return 'end'
|
|
189
217
|
}
|
|
190
218
|
|
|
191
|
-
|
|
219
|
+
let element: HTMLElement | undefined
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
element = DOMEditor.toDOMNode(slateEditor, node)
|
|
223
|
+
} catch (error) {
|
|
224
|
+
console.error(error)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (!element) {
|
|
228
|
+
return undefined
|
|
229
|
+
}
|
|
230
|
+
|
|
192
231
|
const elementRect = element.getBoundingClientRect()
|
|
193
232
|
const top = elementRect.top
|
|
194
233
|
const height = elementRect.height
|
|
@@ -8,10 +8,18 @@
|
|
|
8
8
|
* - The `global` variable is the global scope in Node.js
|
|
9
9
|
*/
|
|
10
10
|
function getGlobalScope() {
|
|
11
|
-
if (typeof globalThis !== 'undefined')
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
if (typeof
|
|
11
|
+
if (typeof globalThis !== 'undefined') {
|
|
12
|
+
return globalThis
|
|
13
|
+
}
|
|
14
|
+
if (typeof window !== 'undefined') {
|
|
15
|
+
return window
|
|
16
|
+
}
|
|
17
|
+
if (typeof self !== 'undefined') {
|
|
18
|
+
return self
|
|
19
|
+
}
|
|
20
|
+
if (typeof global !== 'undefined') {
|
|
21
|
+
return global
|
|
22
|
+
}
|
|
15
23
|
|
|
16
24
|
throw new Error('@portabletext/editor: could not locate global scope')
|
|
17
25
|
}
|
|
@@ -47,7 +47,9 @@ export function sortByPriority<
|
|
|
47
47
|
|
|
48
48
|
// Helper function to add an edge to the graph
|
|
49
49
|
function addEdge(fromId: string, toId: string) {
|
|
50
|
-
if (!graph.has(fromId) || !graph.has(toId))
|
|
50
|
+
if (!graph.has(fromId) || !graph.has(toId)) {
|
|
51
|
+
return
|
|
52
|
+
}
|
|
51
53
|
graph.get(fromId)?.add(toId)
|
|
52
54
|
inDegree.set(toId, (inDegree.get(toId) ?? 0) + 1)
|
|
53
55
|
}
|