@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@portabletext/editor",
3
- "version": "2.21.2",
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.6",
94
- "@vitest/browser-playwright": "^4.0.6",
95
- "@vitest/coverage-istanbul": "^4.0.6",
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.6",
107
+ "vitest": "^4.0.8",
108
108
  "vitest-browser-react": "^2.0.2",
109
109
  "@portabletext/sanity-bridge": "1.2.2",
110
- "@portabletext/test": "^1.0.0",
111
- "racejar": "2.0.0"
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) debug('Updating block', oldBlock, block)
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 (currentBlockChild._key === oldBlockChild?._key) {
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 by Editor B',
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 (_: Context, button: Parameter['button']) => {
225
+ async (context: Context, button: Parameter['button']) => {
226
+ const previousSelection = context.editor.getSnapshot().context.selection
226
227
  await userEvent.keyboard(button)
227
- await new Promise((resolve) => setTimeout(resolve, 100))
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 (_: Context, button: Parameter['button'], times: number) => {
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
- await new Promise((resolve) => setTimeout(resolve, 100))
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 (_: Context, shortcut: Parameter['shortcut']) => {
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
- await new Promise((resolve) => setTimeout(resolve, 100))
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} by Editor B',
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(