@portabletext/editor 2.21.3 → 2.21.5
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.js +19 -13
- package/lib/index.js.map +1 -1
- package/package.json +3 -3
- package/src/editor/sync-machine.ts +9 -5
- package/src/internal-utils/apply-operation-to-portable-text.test.ts +91 -0
- package/src/internal-utils/apply-operation-to-portable-text.ts +6 -3
- package/src/internal-utils/operation-to-patches.test.ts +20 -0
- package/src/internal-utils/operation-to-patches.ts +8 -0
- package/src/internal-utils/portable-text-node.test.ts +132 -0
- package/src/internal-utils/portable-text-node.ts +18 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@portabletext/editor",
|
|
3
|
-
"version": "2.21.
|
|
3
|
+
"version": "2.21.5",
|
|
4
4
|
"description": "Portable Text Editor made in React",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sanity",
|
|
@@ -75,8 +75,8 @@
|
|
|
75
75
|
"xstate": "^5.24.0",
|
|
76
76
|
"@portabletext/block-tools": "^4.0.2",
|
|
77
77
|
"@portabletext/keyboard-shortcuts": "^2.1.0",
|
|
78
|
-
"@portabletext/
|
|
79
|
-
"@portabletext/
|
|
78
|
+
"@portabletext/schema": "^2.0.0",
|
|
79
|
+
"@portabletext/patches": "^2.0.0"
|
|
80
80
|
},
|
|
81
81
|
"devDependencies": {
|
|
82
82
|
"@sanity/diff-match-patch": "^3.2.0",
|
|
@@ -757,8 +757,10 @@ function syncBlock({
|
|
|
757
757
|
}
|
|
758
758
|
|
|
759
759
|
if (validation.valid || validation.resolution?.autoResolve) {
|
|
760
|
-
if (oldBlock._key === block._key) {
|
|
761
|
-
if (debug.enabled)
|
|
760
|
+
if (oldBlock._key === block._key && oldBlock._type === block._type) {
|
|
761
|
+
if (debug.enabled) {
|
|
762
|
+
debug('Updating block', oldBlock, block)
|
|
763
|
+
}
|
|
762
764
|
|
|
763
765
|
Editor.withoutNormalizing(slateEditor, () => {
|
|
764
766
|
withRemoteChanges(slateEditor, () => {
|
|
@@ -908,8 +910,11 @@ function updateBlock({
|
|
|
908
910
|
const path = [index, currentBlockChildIndex]
|
|
909
911
|
|
|
910
912
|
if (isChildChanged) {
|
|
911
|
-
// Update if this is the same child
|
|
912
|
-
if (
|
|
913
|
+
// Update if this is the same child (same key and type)
|
|
914
|
+
if (
|
|
915
|
+
currentBlockChild._key === oldBlockChild?._key &&
|
|
916
|
+
currentBlockChild._type === oldBlockChild?._type
|
|
917
|
+
) {
|
|
913
918
|
debug('Updating changed child', currentBlockChild, oldBlockChild)
|
|
914
919
|
|
|
915
920
|
Transforms.setNodes(slateEditor, currentBlockChild as Partial<Node>, {
|
|
@@ -949,7 +954,6 @@ function updateBlock({
|
|
|
949
954
|
)
|
|
950
955
|
}
|
|
951
956
|
} else if (oldBlockChild) {
|
|
952
|
-
// Replace the child if _key's are different
|
|
953
957
|
debug('Replacing child', currentBlockChild)
|
|
954
958
|
|
|
955
959
|
Transforms.removeNodes(slateEditor, {
|
|
@@ -172,4 +172,95 @@ describe(applyOperationToPortableText.name, () => {
|
|
|
172
172
|
},
|
|
173
173
|
])
|
|
174
174
|
})
|
|
175
|
+
|
|
176
|
+
test('updating block object with a field named "text"', () => {
|
|
177
|
+
const keyGenerator = createTestKeyGenerator()
|
|
178
|
+
const k0 = keyGenerator()
|
|
179
|
+
|
|
180
|
+
expect(
|
|
181
|
+
applyOperationToPortableText(
|
|
182
|
+
createContext(),
|
|
183
|
+
[
|
|
184
|
+
{
|
|
185
|
+
_type: 'quote',
|
|
186
|
+
_key: k0,
|
|
187
|
+
text: 'h',
|
|
188
|
+
},
|
|
189
|
+
],
|
|
190
|
+
{
|
|
191
|
+
type: 'set_node',
|
|
192
|
+
path: [0],
|
|
193
|
+
properties: {
|
|
194
|
+
value: {text: 'h'},
|
|
195
|
+
},
|
|
196
|
+
newProperties: {
|
|
197
|
+
value: {text: 'hello'},
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
),
|
|
201
|
+
).toEqual([
|
|
202
|
+
{
|
|
203
|
+
_type: 'quote',
|
|
204
|
+
_key: k0,
|
|
205
|
+
text: 'hello',
|
|
206
|
+
},
|
|
207
|
+
])
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
test('updating inline object with a field named "text"', () => {
|
|
211
|
+
const keyGenerator = createTestKeyGenerator()
|
|
212
|
+
const k0 = keyGenerator()
|
|
213
|
+
const k1 = keyGenerator()
|
|
214
|
+
const k2 = keyGenerator()
|
|
215
|
+
const k3 = keyGenerator()
|
|
216
|
+
|
|
217
|
+
expect(
|
|
218
|
+
applyOperationToPortableText(
|
|
219
|
+
createContext(),
|
|
220
|
+
[
|
|
221
|
+
{
|
|
222
|
+
_key: k0,
|
|
223
|
+
_type: 'block',
|
|
224
|
+
children: [
|
|
225
|
+
{
|
|
226
|
+
_key: k1,
|
|
227
|
+
_type: 'span',
|
|
228
|
+
text: '',
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
_key: k2,
|
|
232
|
+
_type: 'mention',
|
|
233
|
+
text: 'J',
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
_key: k3,
|
|
237
|
+
_type: 'span',
|
|
238
|
+
text: '',
|
|
239
|
+
},
|
|
240
|
+
],
|
|
241
|
+
},
|
|
242
|
+
],
|
|
243
|
+
{
|
|
244
|
+
type: 'set_node',
|
|
245
|
+
path: [0, 1],
|
|
246
|
+
properties: {
|
|
247
|
+
value: {text: 'J'},
|
|
248
|
+
},
|
|
249
|
+
newProperties: {
|
|
250
|
+
value: {text: 'John Doe'},
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
),
|
|
254
|
+
).toEqual([
|
|
255
|
+
{
|
|
256
|
+
_type: 'block',
|
|
257
|
+
_key: k0,
|
|
258
|
+
children: [
|
|
259
|
+
{_type: 'span', _key: k1, text: ''},
|
|
260
|
+
{_type: 'mention', _key: k2, text: 'John Doe'},
|
|
261
|
+
{_type: 'span', _key: k3, text: ''},
|
|
262
|
+
],
|
|
263
|
+
},
|
|
264
|
+
])
|
|
265
|
+
})
|
|
175
266
|
})
|
|
@@ -107,7 +107,7 @@ function applyOperationToPortableTextDraft(
|
|
|
107
107
|
break
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
if (isPartialSpanNode(insertedNode)) {
|
|
110
|
+
if (isPartialSpanNode(context, insertedNode)) {
|
|
111
111
|
// Text nodes can be inserted as is
|
|
112
112
|
|
|
113
113
|
parent.children.splice(index, 0, insertedNode)
|
|
@@ -161,7 +161,10 @@ function applyOperationToPortableTextDraft(
|
|
|
161
161
|
|
|
162
162
|
const index = path[path.length - 1]
|
|
163
163
|
|
|
164
|
-
if (
|
|
164
|
+
if (
|
|
165
|
+
isPartialSpanNode(context, node) &&
|
|
166
|
+
isPartialSpanNode(context, prev)
|
|
167
|
+
) {
|
|
165
168
|
prev.text += node.text
|
|
166
169
|
} else if (
|
|
167
170
|
isTextBlockNode(context, node) &&
|
|
@@ -342,7 +345,7 @@ function applyOperationToPortableTextDraft(
|
|
|
342
345
|
break
|
|
343
346
|
}
|
|
344
347
|
|
|
345
|
-
if (isPartialSpanNode(node)) {
|
|
348
|
+
if (isPartialSpanNode(context, node)) {
|
|
346
349
|
for (const key in newProperties) {
|
|
347
350
|
if (key === 'text') {
|
|
348
351
|
break
|
|
@@ -141,6 +141,16 @@ describe('operationToPatches', () => {
|
|
|
141
141
|
),
|
|
142
142
|
).toMatchInlineSnapshot(`
|
|
143
143
|
[
|
|
144
|
+
{
|
|
145
|
+
"path": [
|
|
146
|
+
{
|
|
147
|
+
"_key": "1f2e64b47787",
|
|
148
|
+
},
|
|
149
|
+
"children",
|
|
150
|
+
],
|
|
151
|
+
"type": "setIfMissing",
|
|
152
|
+
"value": [],
|
|
153
|
+
},
|
|
144
154
|
{
|
|
145
155
|
"items": [
|
|
146
156
|
{
|
|
@@ -285,6 +295,16 @@ describe('operationToPatches', () => {
|
|
|
285
295
|
),
|
|
286
296
|
).toMatchInlineSnapshot(`
|
|
287
297
|
[
|
|
298
|
+
{
|
|
299
|
+
"path": [
|
|
300
|
+
{
|
|
301
|
+
"_key": "1f2e64b47787",
|
|
302
|
+
},
|
|
303
|
+
"children",
|
|
304
|
+
],
|
|
305
|
+
"type": "setIfMissing",
|
|
306
|
+
"value": [],
|
|
307
|
+
},
|
|
288
308
|
{
|
|
289
309
|
"items": [
|
|
290
310
|
{
|
|
@@ -314,6 +314,9 @@ export function insertNodePatch(
|
|
|
314
314
|
node._type = 'span'
|
|
315
315
|
node.marks = []
|
|
316
316
|
}
|
|
317
|
+
|
|
318
|
+
// Defensive setIfMissing to ensure children array exists before inserting
|
|
319
|
+
const setIfMissingPatch = setIfMissing([], [{_key: block._key}, 'children'])
|
|
317
320
|
const blk = fromSlateValue(
|
|
318
321
|
[
|
|
319
322
|
{
|
|
@@ -326,6 +329,7 @@ export function insertNodePatch(
|
|
|
326
329
|
)[0] as PortableTextTextBlock
|
|
327
330
|
const child = blk.children[0]
|
|
328
331
|
return [
|
|
332
|
+
setIfMissingPatch,
|
|
329
333
|
insert([child], position, [
|
|
330
334
|
{_key: block._key},
|
|
331
335
|
'children',
|
|
@@ -389,6 +393,8 @@ export function splitNodePatch(
|
|
|
389
393
|
)[0] as PortableTextTextBlock
|
|
390
394
|
).children
|
|
391
395
|
|
|
396
|
+
// Defensive setIfMissing to ensure children array exists before inserting
|
|
397
|
+
patches.push(setIfMissing([], [{_key: splitBlock._key}, 'children']))
|
|
392
398
|
patches.push(
|
|
393
399
|
insert(targetSpans, 'after', [
|
|
394
400
|
{_key: splitBlock._key},
|
|
@@ -563,6 +569,8 @@ export function moveNodePatch(
|
|
|
563
569
|
fromSlateValue([block], schema.block.name)[0] as PortableTextTextBlock
|
|
564
570
|
).children[operation.path[1]]
|
|
565
571
|
patches.push(unset([{_key: block._key}, 'children', {_key: child._key}]))
|
|
572
|
+
// Defensive setIfMissing to ensure children array exists before inserting
|
|
573
|
+
patches.push(setIfMissing([], [{_key: targetBlock._key}, 'children']))
|
|
566
574
|
patches.push(
|
|
567
575
|
insert([childToInsert], position, [
|
|
568
576
|
{_key: targetBlock._key},
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import {compileSchema, defineSchema} from '@portabletext/schema'
|
|
2
|
+
import {describe, expect, test} from 'vitest'
|
|
3
|
+
import {isObjectNode, isPartialSpanNode, isSpanNode} from './portable-text-node'
|
|
4
|
+
|
|
5
|
+
const schema = compileSchema(defineSchema({}))
|
|
6
|
+
|
|
7
|
+
describe(isPartialSpanNode.name, () => {
|
|
8
|
+
test('object with only text property', () => {
|
|
9
|
+
expect(isPartialSpanNode({schema}, {text: 'Hello'})).toBe(true)
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
test('non-objects', () => {
|
|
13
|
+
expect(isPartialSpanNode({schema}, null)).toBe(false)
|
|
14
|
+
expect(isPartialSpanNode({schema}, undefined)).toBe(false)
|
|
15
|
+
expect(isPartialSpanNode({schema}, 'text')).toBe(false)
|
|
16
|
+
expect(isPartialSpanNode({schema}, 123)).toBe(false)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test('text is not a string', () => {
|
|
20
|
+
expect(isPartialSpanNode({schema}, {text: 123})).toBe(false)
|
|
21
|
+
expect(isPartialSpanNode({schema}, {text: null})).toBe(false)
|
|
22
|
+
expect(isPartialSpanNode({schema}, {text: undefined})).toBe(false)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('inline object with text field and _type', () => {
|
|
26
|
+
expect(
|
|
27
|
+
isPartialSpanNode(
|
|
28
|
+
{schema},
|
|
29
|
+
{_type: 'mention', _key: 'abc123', text: 'John Doe'},
|
|
30
|
+
),
|
|
31
|
+
).toBe(false)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('block object with text field and _type', () => {
|
|
35
|
+
expect(
|
|
36
|
+
isPartialSpanNode(
|
|
37
|
+
{schema},
|
|
38
|
+
{
|
|
39
|
+
_type: 'quote',
|
|
40
|
+
_key: 'abc123',
|
|
41
|
+
text: 'Hello world',
|
|
42
|
+
source: 'Anonymous',
|
|
43
|
+
},
|
|
44
|
+
),
|
|
45
|
+
).toBe(false)
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
describe(isSpanNode.name, () => {
|
|
50
|
+
test('span with _type="span"', () => {
|
|
51
|
+
expect(isSpanNode({schema}, {_type: 'span', text: 'Hello'})).toBe(true)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('partial span (no _type, has text)', () => {
|
|
55
|
+
expect(isSpanNode({schema}, {text: 'Hello'})).toBe(true)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('object with children', () => {
|
|
59
|
+
expect(
|
|
60
|
+
isSpanNode({schema}, {_type: 'span', text: 'Hello', children: []}),
|
|
61
|
+
).toBe(false)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('object with different _type', () => {
|
|
65
|
+
expect(isSpanNode({schema}, {_type: 'mention', text: 'Hello'})).toBe(false)
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
describe(isObjectNode.name, () => {
|
|
70
|
+
test('inline object', () => {
|
|
71
|
+
expect(
|
|
72
|
+
isObjectNode(
|
|
73
|
+
{schema},
|
|
74
|
+
{_type: 'stock-ticker', _key: 'abc', symbol: 'AAPL'},
|
|
75
|
+
),
|
|
76
|
+
).toBe(true)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('block object', () => {
|
|
80
|
+
expect(
|
|
81
|
+
isObjectNode(
|
|
82
|
+
{schema},
|
|
83
|
+
{_type: 'image', _key: 'abc', src: 'https://example.com'},
|
|
84
|
+
),
|
|
85
|
+
).toBe(true)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('inline object with text field', () => {
|
|
89
|
+
expect(
|
|
90
|
+
isObjectNode(
|
|
91
|
+
{schema},
|
|
92
|
+
{_type: 'mention', _key: 'abc123', text: 'John Doe'},
|
|
93
|
+
),
|
|
94
|
+
).toBe(true)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test('block object with text field', () => {
|
|
98
|
+
expect(
|
|
99
|
+
isObjectNode(
|
|
100
|
+
{schema},
|
|
101
|
+
{
|
|
102
|
+
_type: 'quote',
|
|
103
|
+
_key: 'abc123',
|
|
104
|
+
text: 'Hello world',
|
|
105
|
+
source: 'Anonymous',
|
|
106
|
+
},
|
|
107
|
+
),
|
|
108
|
+
).toBe(true)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
test('span', () => {
|
|
112
|
+
expect(
|
|
113
|
+
isObjectNode(
|
|
114
|
+
{schema},
|
|
115
|
+
{_type: 'span', _key: 'abc', text: 'Hello', marks: []},
|
|
116
|
+
),
|
|
117
|
+
).toBe(false)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
test('text block', () => {
|
|
121
|
+
expect(
|
|
122
|
+
isObjectNode(
|
|
123
|
+
{schema},
|
|
124
|
+
{
|
|
125
|
+
_type: 'block',
|
|
126
|
+
_key: 'abc',
|
|
127
|
+
children: [{_type: 'span', text: 'Hello'}],
|
|
128
|
+
},
|
|
129
|
+
),
|
|
130
|
+
).toBe(false)
|
|
131
|
+
})
|
|
132
|
+
})
|
|
@@ -79,13 +79,23 @@ export type PartialSpanNode = {
|
|
|
79
79
|
[other: string]: unknown
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
export function isPartialSpanNode(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
82
|
+
export function isPartialSpanNode(
|
|
83
|
+
context: {schema: EditorSchema},
|
|
84
|
+
node: unknown,
|
|
85
|
+
): node is PartialSpanNode {
|
|
86
|
+
if (typeof node !== 'object' || node === null) {
|
|
87
|
+
return false
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!('text' in node) || typeof node.text !== 'string') {
|
|
91
|
+
return false
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if ('_type' in node && node._type !== context.schema.span.name) {
|
|
95
|
+
return false
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return true
|
|
89
99
|
}
|
|
90
100
|
|
|
91
101
|
//////////
|
|
@@ -104,7 +114,7 @@ export function isObjectNode(
|
|
|
104
114
|
!isEditorNode(node) &&
|
|
105
115
|
!isTextBlockNode(context, node) &&
|
|
106
116
|
!isSpanNode(context, node) &&
|
|
107
|
-
!isPartialSpanNode(node)
|
|
117
|
+
!isPartialSpanNode(context, node)
|
|
108
118
|
)
|
|
109
119
|
}
|
|
110
120
|
|