@portabletext/editor 2.21.2 → 2.21.4
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 +39 -7
- package/lib/index.js.map +1 -1
- package/package.json +8 -8
- package/src/editor/sync-machine.ts +9 -5
- package/src/internal-utils/applyPatch.ts +46 -1
- package/src/internal-utils/operation-to-patches.test.ts +20 -0
- package/src/internal-utils/operation-to-patches.ts +8 -0
- package/src/test/vitest/step-definitions.tsx +33 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@portabletext/editor",
|
|
3
|
-
"version": "2.21.
|
|
3
|
+
"version": "2.21.4",
|
|
4
4
|
"description": "Portable Text Editor made in React",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sanity",
|
|
@@ -73,9 +73,9 @@
|
|
|
73
73
|
"slate-dom": "^0.119.0",
|
|
74
74
|
"slate-react": "0.119.0",
|
|
75
75
|
"xstate": "^5.24.0",
|
|
76
|
-
"@portabletext/block-tools": "^4.0.2",
|
|
77
76
|
"@portabletext/keyboard-shortcuts": "^2.1.0",
|
|
78
77
|
"@portabletext/patches": "^2.0.0",
|
|
78
|
+
"@portabletext/block-tools": "^4.0.2",
|
|
79
79
|
"@portabletext/schema": "^2.0.0"
|
|
80
80
|
},
|
|
81
81
|
"devDependencies": {
|
|
@@ -90,9 +90,9 @@
|
|
|
90
90
|
"@types/react": "^19.2.2",
|
|
91
91
|
"@types/react-dom": "^19.2.2",
|
|
92
92
|
"@vitejs/plugin-react": "^5.0.4",
|
|
93
|
-
"@vitest/browser": "^4.0.
|
|
94
|
-
"@vitest/browser-playwright": "^4.0.
|
|
95
|
-
"@vitest/coverage-istanbul": "^4.0.
|
|
93
|
+
"@vitest/browser": "^4.0.8",
|
|
94
|
+
"@vitest/browser-playwright": "^4.0.8",
|
|
95
|
+
"@vitest/coverage-istanbul": "^4.0.8",
|
|
96
96
|
"babel-plugin-react-compiler": "1.0.0",
|
|
97
97
|
"eslint": "^9.38.0",
|
|
98
98
|
"eslint-formatter-gha": "^1.6.0",
|
|
@@ -104,11 +104,11 @@
|
|
|
104
104
|
"typescript": "5.9.3",
|
|
105
105
|
"typescript-eslint": "^8.46.1",
|
|
106
106
|
"vite": "^7.1.12",
|
|
107
|
-
"vitest": "^4.0.
|
|
107
|
+
"vitest": "^4.0.8",
|
|
108
108
|
"vitest-browser-react": "^2.0.2",
|
|
109
109
|
"@portabletext/sanity-bridge": "1.2.2",
|
|
110
|
-
"
|
|
111
|
-
"
|
|
110
|
+
"racejar": "2.0.0",
|
|
111
|
+
"@portabletext/test": "^1.0.0"
|
|
112
112
|
},
|
|
113
113
|
"peerDependencies": {
|
|
114
114
|
"@portabletext/sanity-bridge": "^1.2.2",
|
|
@@ -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, {
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
parsePatch,
|
|
17
17
|
} from '@sanity/diff-match-patch'
|
|
18
18
|
import type {Path, PortableTextBlock, PortableTextChild} from '@sanity/types'
|
|
19
|
-
import {Element, Node, Text, Transforms, type Descendant} from 'slate'
|
|
19
|
+
import {Editor, Element, Node, Text, Transforms, type Descendant} from 'slate'
|
|
20
20
|
import type {EditorSchema} from '../editor/editor-schema'
|
|
21
21
|
import {KEY_TO_SLATE_ELEMENT} from '../editor/weakMaps'
|
|
22
22
|
import type {PortableTextSlateEditor} from '../types/editor'
|
|
@@ -202,6 +202,51 @@ function setPatch(editor: PortableTextSlateEditor, patch: SetPatch) {
|
|
|
202
202
|
|
|
203
203
|
const isTextBlock = editor.isTextBlock(block.node)
|
|
204
204
|
|
|
205
|
+
if (patch.path.length === 1) {
|
|
206
|
+
const updatedBlock = applyAll(block.node, [
|
|
207
|
+
{
|
|
208
|
+
...patch,
|
|
209
|
+
path: patch.path.slice(1),
|
|
210
|
+
},
|
|
211
|
+
])
|
|
212
|
+
|
|
213
|
+
if (editor.isTextBlock(block.node) && Element.isElement(updatedBlock)) {
|
|
214
|
+
Transforms.setNodes(editor, updatedBlock, {at: [block.index]})
|
|
215
|
+
|
|
216
|
+
const previousSelection = editor.selection
|
|
217
|
+
|
|
218
|
+
// Remove the previous children
|
|
219
|
+
for (const [_, childPath] of Editor.nodes(editor, {
|
|
220
|
+
at: [block.index],
|
|
221
|
+
reverse: true,
|
|
222
|
+
mode: 'lowest',
|
|
223
|
+
})) {
|
|
224
|
+
Transforms.removeNodes(editor, {at: childPath})
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Insert the new children
|
|
228
|
+
Transforms.insertNodes(editor, updatedBlock.children, {
|
|
229
|
+
at: [block.index, 0],
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
// Restore the selection
|
|
233
|
+
if (previousSelection) {
|
|
234
|
+
// Update the selection on the editor object
|
|
235
|
+
Transforms.setSelection(editor, previousSelection)
|
|
236
|
+
// Actively select the previous selection
|
|
237
|
+
Transforms.select(editor, previousSelection)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return true
|
|
241
|
+
} else {
|
|
242
|
+
Transforms.setNodes(editor, updatedBlock as Partial<Node>, {
|
|
243
|
+
at: [block.index],
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
return true
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
205
250
|
if (isTextBlock && patch.path[1] !== 'children') {
|
|
206
251
|
const updatedBlock = applyAll(block.node, [
|
|
207
252
|
{
|
|
@@ -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},
|
|
@@ -190,7 +190,7 @@ export const stepDefinitions = [
|
|
|
190
190
|
await userEvent.type(context.locator, text)
|
|
191
191
|
}),
|
|
192
192
|
When(
|
|
193
|
-
'{string} is typed
|
|
193
|
+
'{string} is typed in Editor B',
|
|
194
194
|
async (context: Context, text: string) => {
|
|
195
195
|
await userEvent.type(context.locatorB, text)
|
|
196
196
|
},
|
|
@@ -222,23 +222,40 @@ export const stepDefinitions = [
|
|
|
222
222
|
*/
|
|
223
223
|
When(
|
|
224
224
|
'{button} is pressed',
|
|
225
|
-
async (
|
|
225
|
+
async (context: Context, button: Parameter['button']) => {
|
|
226
|
+
const previousSelection = context.editor.getSnapshot().context.selection
|
|
226
227
|
await userEvent.keyboard(button)
|
|
227
|
-
|
|
228
|
+
|
|
229
|
+
await vi.waitFor(() => {
|
|
230
|
+
const currentSelection = context.editor.getSnapshot().context.selection
|
|
231
|
+
|
|
232
|
+
if (currentSelection) {
|
|
233
|
+
expect(currentSelection).not.toBe(previousSelection)
|
|
234
|
+
}
|
|
235
|
+
})
|
|
228
236
|
},
|
|
229
237
|
),
|
|
230
238
|
When(
|
|
231
239
|
'{button} is pressed {int} times',
|
|
232
|
-
async (
|
|
240
|
+
async (context: Context, button: Parameter['button'], times: number) => {
|
|
233
241
|
for (let i = 0; i < times; i++) {
|
|
242
|
+
const previousSelection = context.editor.getSnapshot().context.selection
|
|
234
243
|
await userEvent.keyboard(button)
|
|
235
|
-
|
|
244
|
+
|
|
245
|
+
await vi.waitFor(() => {
|
|
246
|
+
const currentSelection =
|
|
247
|
+
context.editor.getSnapshot().context.selection
|
|
248
|
+
|
|
249
|
+
if (currentSelection) {
|
|
250
|
+
expect(currentSelection).not.toBe(previousSelection)
|
|
251
|
+
}
|
|
252
|
+
})
|
|
236
253
|
}
|
|
237
254
|
},
|
|
238
255
|
),
|
|
239
256
|
When(
|
|
240
257
|
'{shortcut} is pressed',
|
|
241
|
-
async (
|
|
258
|
+
async (context: Context, shortcut: Parameter['shortcut']) => {
|
|
242
259
|
const shortcuts: Record<Parameter['shortcut'], string> = {
|
|
243
260
|
'deleteWord.backward': IS_MAC
|
|
244
261
|
? '{Alt>}{Backspace}{/Alt}'
|
|
@@ -248,8 +265,16 @@ export const stepDefinitions = [
|
|
|
248
265
|
: '{Control>}{Delete}{/Control}',
|
|
249
266
|
}
|
|
250
267
|
|
|
268
|
+
const previousSelection = context.editor.getSnapshot().context.selection
|
|
251
269
|
await userEvent.keyboard(shortcuts[shortcut])
|
|
252
|
-
|
|
270
|
+
|
|
271
|
+
await vi.waitFor(() => {
|
|
272
|
+
const currentSelection = context.editor.getSnapshot().context.selection
|
|
273
|
+
|
|
274
|
+
if (currentSelection) {
|
|
275
|
+
expect(currentSelection).not.toBe(previousSelection)
|
|
276
|
+
}
|
|
277
|
+
})
|
|
253
278
|
},
|
|
254
279
|
),
|
|
255
280
|
|
|
@@ -315,7 +340,7 @@ export const stepDefinitions = [
|
|
|
315
340
|
},
|
|
316
341
|
),
|
|
317
342
|
When(
|
|
318
|
-
'the caret is put after {string}
|
|
343
|
+
'the caret is put after {string} in Editor B',
|
|
319
344
|
async (context: Context, text: string) => {
|
|
320
345
|
await vi.waitFor(() => {
|
|
321
346
|
const selection = getSelectionAfterText(
|