@wordpress/core-data 7.40.1 → 7.40.2-next.v.202602271551.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.
Files changed (136) hide show
  1. package/build/actions.cjs +23 -29
  2. package/build/actions.cjs.map +2 -2
  3. package/build/awareness/block-lookup.cjs +103 -0
  4. package/build/awareness/block-lookup.cjs.map +7 -0
  5. package/build/awareness/post-editor-awareness.cjs +45 -7
  6. package/build/awareness/post-editor-awareness.cjs.map +3 -3
  7. package/build/entities.cjs +63 -59
  8. package/build/entities.cjs.map +2 -2
  9. package/build/entity-types/icon.cjs +19 -0
  10. package/build/entity-types/icon.cjs.map +7 -0
  11. package/build/entity-types/index.cjs.map +1 -1
  12. package/build/hooks/use-post-editor-awareness-state.cjs +12 -8
  13. package/build/hooks/use-post-editor-awareness-state.cjs.map +2 -2
  14. package/build/private-actions.cjs +0 -8
  15. package/build/private-actions.cjs.map +2 -2
  16. package/build/private-apis.cjs +1 -1
  17. package/build/private-apis.cjs.map +1 -1
  18. package/build/private-selectors.cjs +1 -9
  19. package/build/private-selectors.cjs.map +2 -2
  20. package/build/reducer.cjs +0 -10
  21. package/build/reducer.cjs.map +2 -2
  22. package/build/resolvers.cjs +116 -125
  23. package/build/resolvers.cjs.map +2 -2
  24. package/build/selectors.cjs.map +2 -2
  25. package/build/sync.cjs +1 -7
  26. package/build/sync.cjs.map +2 -2
  27. package/build/types.cjs.map +1 -1
  28. package/build/utils/crdt-blocks.cjs +50 -31
  29. package/build/utils/crdt-blocks.cjs.map +2 -2
  30. package/build/utils/crdt-selection.cjs +47 -19
  31. package/build/utils/crdt-selection.cjs.map +2 -2
  32. package/build/utils/crdt-user-selections.cjs +78 -22
  33. package/build/utils/crdt-user-selections.cjs.map +3 -3
  34. package/build/utils/crdt.cjs +12 -1
  35. package/build/utils/crdt.cjs.map +2 -2
  36. package/build-module/actions.mjs +23 -29
  37. package/build-module/actions.mjs.map +2 -2
  38. package/build-module/awareness/block-lookup.mjs +77 -0
  39. package/build-module/awareness/block-lookup.mjs.map +7 -0
  40. package/build-module/awareness/post-editor-awareness.mjs +47 -8
  41. package/build-module/awareness/post-editor-awareness.mjs.map +3 -3
  42. package/build-module/entities.mjs +65 -60
  43. package/build-module/entities.mjs.map +2 -2
  44. package/build-module/entity-types/icon.mjs +1 -0
  45. package/build-module/entity-types/icon.mjs.map +7 -0
  46. package/build-module/hooks/use-post-editor-awareness-state.mjs +10 -6
  47. package/build-module/hooks/use-post-editor-awareness-state.mjs.map +2 -2
  48. package/build-module/private-actions.mjs +0 -7
  49. package/build-module/private-actions.mjs.map +2 -2
  50. package/build-module/private-apis.mjs +2 -2
  51. package/build-module/private-apis.mjs.map +1 -1
  52. package/build-module/private-selectors.mjs +2 -12
  53. package/build-module/private-selectors.mjs.map +2 -2
  54. package/build-module/reducer.mjs +0 -9
  55. package/build-module/reducer.mjs.map +2 -2
  56. package/build-module/resolvers.mjs +116 -124
  57. package/build-module/resolvers.mjs.map +2 -2
  58. package/build-module/selectors.mjs.map +2 -2
  59. package/build-module/sync.mjs +1 -5
  60. package/build-module/sync.mjs.map +2 -2
  61. package/build-module/utils/crdt-blocks.mjs +50 -31
  62. package/build-module/utils/crdt-blocks.mjs.map +2 -2
  63. package/build-module/utils/crdt-selection.mjs +46 -19
  64. package/build-module/utils/crdt-selection.mjs.map +2 -2
  65. package/build-module/utils/crdt-user-selections.mjs +77 -22
  66. package/build-module/utils/crdt-user-selections.mjs.map +2 -2
  67. package/build-module/utils/crdt.mjs +16 -6
  68. package/build-module/utils/crdt.mjs.map +2 -2
  69. package/build-types/actions.d.ts.map +1 -1
  70. package/build-types/awareness/block-lookup.d.ts +29 -0
  71. package/build-types/awareness/block-lookup.d.ts.map +1 -0
  72. package/build-types/awareness/post-editor-awareness.d.ts +18 -5
  73. package/build-types/awareness/post-editor-awareness.d.ts.map +1 -1
  74. package/build-types/awareness/test/block-lookup.d.ts +2 -0
  75. package/build-types/awareness/test/block-lookup.d.ts.map +1 -0
  76. package/build-types/entities.d.ts +16 -0
  77. package/build-types/entities.d.ts.map +1 -1
  78. package/build-types/entity-types/icon.d.ts +25 -0
  79. package/build-types/entity-types/icon.d.ts.map +1 -0
  80. package/build-types/entity-types/index.d.ts +3 -2
  81. package/build-types/entity-types/index.d.ts.map +1 -1
  82. package/build-types/hooks/use-post-editor-awareness-state.d.ts +11 -6
  83. package/build-types/hooks/use-post-editor-awareness-state.d.ts.map +1 -1
  84. package/build-types/index.d.ts.map +1 -1
  85. package/build-types/private-actions.d.ts +0 -8
  86. package/build-types/private-actions.d.ts.map +1 -1
  87. package/build-types/private-selectors.d.ts +1 -8
  88. package/build-types/private-selectors.d.ts.map +1 -1
  89. package/build-types/reducer.d.ts +0 -11
  90. package/build-types/reducer.d.ts.map +1 -1
  91. package/build-types/resolvers.d.ts +0 -3
  92. package/build-types/resolvers.d.ts.map +1 -1
  93. package/build-types/selectors.d.ts +0 -6
  94. package/build-types/selectors.d.ts.map +1 -1
  95. package/build-types/sync.d.ts +2 -2
  96. package/build-types/sync.d.ts.map +1 -1
  97. package/build-types/types.d.ts +14 -5
  98. package/build-types/types.d.ts.map +1 -1
  99. package/build-types/utils/crdt-blocks.d.ts +1 -1
  100. package/build-types/utils/crdt-blocks.d.ts.map +1 -1
  101. package/build-types/utils/crdt-selection.d.ts +10 -0
  102. package/build-types/utils/crdt-selection.d.ts.map +1 -1
  103. package/build-types/utils/crdt-user-selections.d.ts +21 -4
  104. package/build-types/utils/crdt-user-selections.d.ts.map +1 -1
  105. package/build-types/utils/crdt.d.ts +1 -0
  106. package/build-types/utils/crdt.d.ts.map +1 -1
  107. package/build-types/utils/test/crdt-user-selections.d.ts +2 -0
  108. package/build-types/utils/test/crdt-user-selections.d.ts.map +1 -0
  109. package/package.json +18 -18
  110. package/src/actions.js +39 -45
  111. package/src/awareness/block-lookup.ts +169 -0
  112. package/src/awareness/post-editor-awareness.ts +68 -11
  113. package/src/awareness/test/block-lookup.ts +504 -0
  114. package/src/awareness/test/post-editor-awareness.ts +662 -38
  115. package/src/entities.js +71 -62
  116. package/src/entity-types/icon.ts +30 -0
  117. package/src/entity-types/index.ts +3 -0
  118. package/src/hooks/test/use-post-editor-awareness-state.ts +21 -14
  119. package/src/hooks/use-post-editor-awareness-state.ts +22 -13
  120. package/src/private-actions.js +0 -14
  121. package/src/private-apis.js +2 -2
  122. package/src/private-selectors.ts +3 -22
  123. package/src/reducer.js +0 -17
  124. package/src/resolvers.js +158 -171
  125. package/src/selectors.ts +0 -7
  126. package/src/sync.ts +0 -4
  127. package/src/test/entities.js +39 -10
  128. package/src/test/resolvers.js +155 -81
  129. package/src/types.ts +23 -5
  130. package/src/utils/crdt-blocks.ts +113 -47
  131. package/src/utils/crdt-selection.ts +87 -25
  132. package/src/utils/crdt-user-selections.ts +129 -47
  133. package/src/utils/crdt.ts +23 -7
  134. package/src/utils/test/crdt-blocks.ts +591 -0
  135. package/src/utils/test/crdt-user-selections.ts +894 -0
  136. package/src/utils/test/crdt.ts +136 -10
@@ -88,6 +88,41 @@ function convertYSelectionToBlockSelection(
88
88
  return null;
89
89
  }
90
90
 
91
+ /**
92
+ * Convert a YFullSelection to a WPSelection by resolving relative positions
93
+ * and verifying the blocks exist in the document.
94
+ * @param yFullSelection The YFullSelection to convert
95
+ * @param ydoc The Y.Doc to resolve positions against
96
+ * @return The converted WPSelection, or null if the conversion fails
97
+ */
98
+ function convertYFullSelectionToWPSelection(
99
+ yFullSelection: YFullSelection,
100
+ ydoc: Y.Doc
101
+ ): WPSelection | null {
102
+ const { start, end } = yFullSelection;
103
+ const startBlock = findBlockByClientIdInDoc( start.clientId, ydoc );
104
+ const endBlock = findBlockByClientIdInDoc( end.clientId, ydoc );
105
+
106
+ if ( ! startBlock || ! endBlock ) {
107
+ return null;
108
+ }
109
+
110
+ const startBlockSelection = convertYSelectionToBlockSelection(
111
+ start,
112
+ ydoc
113
+ );
114
+ const endBlockSelection = convertYSelectionToBlockSelection( end, ydoc );
115
+
116
+ if ( startBlockSelection === null || endBlockSelection === null ) {
117
+ return null;
118
+ }
119
+
120
+ return {
121
+ selectionStart: startBlockSelection,
122
+ selectionEnd: endBlockSelection,
123
+ };
124
+ }
125
+
91
126
  /**
92
127
  * Given a Y.Doc and a selection history, find the most recent selection
93
128
  * that exists in the document. Skip any selections that are not in the document.
@@ -99,34 +134,14 @@ function findSelectionFromHistory(
99
134
  ydoc: Y.Doc,
100
135
  selectionHistory: YFullSelection[]
101
136
  ): WPSelection | null {
102
- // Try each position until we find one that exists in the document
103
137
  for ( const positionToTry of selectionHistory ) {
104
- const { start, end } = positionToTry;
105
- const startBlock = findBlockByClientIdInDoc( start.clientId, ydoc );
106
- const endBlock = findBlockByClientIdInDoc( end.clientId, ydoc );
107
-
108
- if ( ! startBlock || ! endBlock ) {
109
- // This block no longer exists, skip it.
110
- continue;
111
- }
112
-
113
- const startBlockSelection = convertYSelectionToBlockSelection(
114
- start,
138
+ const result = convertYFullSelectionToWPSelection(
139
+ positionToTry,
115
140
  ydoc
116
141
  );
117
- const endBlockSelection = convertYSelectionToBlockSelection(
118
- end,
119
- ydoc
120
- );
121
-
122
- if ( startBlockSelection === null || endBlockSelection === null ) {
123
- continue;
142
+ if ( result !== null ) {
143
+ return result;
124
144
  }
125
-
126
- return {
127
- selectionStart: startBlockSelection,
128
- selectionEnd: endBlockSelection,
129
- };
130
145
  }
131
146
 
132
147
  return null;
@@ -170,7 +185,9 @@ export function restoreSelection(
170
185
  const isBeginningOfEmptyBlock =
171
186
  0 === selectionStart.offset &&
172
187
  0 === selectionEnd.offset &&
173
- isBlockEmpty;
188
+ isBlockEmpty &&
189
+ ! selectionStart.attributeKey &&
190
+ ! selectionEnd.attributeKey;
174
191
 
175
192
  if ( isBeginningOfEmptyBlock ) {
176
193
  // Case 2a: When the content in a block has been removed after an
@@ -203,3 +220,48 @@ export function restoreSelection(
203
220
  resetSelection( selectionEnd, selectionEnd, 0 );
204
221
  }
205
222
  }
223
+
224
+ /**
225
+ * If the latest selection has been shifted by remote edits, resolve and return
226
+ * it as a WPSelection. Returns null when the history is empty or neither
227
+ * endpoint has moved.
228
+ *
229
+ * @param ydoc The Y.Doc to resolve positions against
230
+ * @param selectionHistory The selection history to check
231
+ * @return The shifted WPSelection, or null if nothing moved.
232
+ */
233
+ export function getShiftedSelection(
234
+ ydoc: Y.Doc,
235
+ selectionHistory: YFullSelection[]
236
+ ): WPSelection | null {
237
+ if ( selectionHistory.length === 0 ) {
238
+ return null;
239
+ }
240
+
241
+ const { start, end } = selectionHistory[ 0 ];
242
+
243
+ // Block-level selections have no offset that can shift.
244
+ if (
245
+ start.type === YSelectionType.BlockSelection ||
246
+ end.type === YSelectionType.BlockSelection
247
+ ) {
248
+ return null;
249
+ }
250
+
251
+ const selectionStart = convertYSelectionToBlockSelection( start, ydoc );
252
+ const selectionEnd = convertYSelectionToBlockSelection( end, ydoc );
253
+
254
+ if ( ! selectionStart || ! selectionEnd ) {
255
+ return null;
256
+ }
257
+
258
+ // Only dispatch if at least one endpoint actually moved.
259
+ const startShifted = selectionStart.offset !== start.offset;
260
+ const endShifted = selectionEnd.offset !== end.offset;
261
+
262
+ if ( ! startShifted && ! endShifted ) {
263
+ return null;
264
+ }
265
+
266
+ return { selectionStart, selectionEnd };
267
+ }
@@ -1,7 +1,10 @@
1
1
  /**
2
2
  * WordPress dependencies
3
3
  */
4
+ import { select } from '@wordpress/data';
4
5
  import { Y } from '@wordpress/sync';
6
+ // @ts-ignore No exported types for block editor store selectors.
7
+ import { store as blockEditorStore } from '@wordpress/block-editor';
5
8
 
6
9
  /**
7
10
  * Internal dependencies
@@ -11,6 +14,7 @@ import type { YPostRecord } from './crdt';
11
14
  import type { YBlock, YBlocks } from './crdt-blocks';
12
15
  import { getRootMap } from './crdt-utils';
13
16
  import type {
17
+ AbsoluteBlockIndexPath,
14
18
  WPBlockSelection,
15
19
  SelectionState,
16
20
  SelectionNone,
@@ -35,6 +39,11 @@ export enum SelectionType {
35
39
  /**
36
40
  * Converts WordPress block editor selection to a SelectionState.
37
41
  *
42
+ * Uses getBlockPathForLocalClientId to locate blocks in the Yjs document by
43
+ * their tree position (index path) rather than clientId, since clientIds may
44
+ * differ between the block-editor store and the Yjs document (e.g. in "Show
45
+ * Template" mode).
46
+ *
38
47
  * @param selectionStart - The start position of the selection
39
48
  * @param selectionEnd - The end position of the selection
40
49
  * @param yDoc - The Yjs document
@@ -46,15 +55,15 @@ export function getSelectionState(
46
55
  yDoc: Y.Doc
47
56
  ): SelectionState {
48
57
  const ymap = getRootMap< YPostRecord >( yDoc, CRDT_RECORD_MAP_KEY );
49
- const yBlocks = ymap.get( 'blocks' ) ?? new Y.Array< YBlock >();
58
+ const yBlocks = ymap.get( 'blocks' );
50
59
 
51
60
  const isSelectionEmpty = Object.keys( selectionStart ).length === 0;
52
61
  const noSelection: SelectionNone = {
53
62
  type: SelectionType.None,
54
63
  };
55
64
 
56
- if ( isSelectionEmpty ) {
57
- // Case 1: No selection
65
+ if ( isSelectionEmpty || ! yBlocks ) {
66
+ // Case 1: No selection, or no blocks in the document.
58
67
  return noSelection;
59
68
  }
60
69
 
@@ -70,9 +79,18 @@ export function getSelectionState(
70
79
 
71
80
  if ( isSelectionAWholeBlock ) {
72
81
  // Case 2: A whole block is selected.
82
+ const path = getBlockPathForLocalClientId( selectionStart.clientId );
83
+ const blockPosition = path
84
+ ? createRelativePositionForBlockPath( path, yBlocks )
85
+ : null;
86
+
87
+ if ( ! blockPosition ) {
88
+ return noSelection;
89
+ }
90
+
73
91
  return {
74
92
  type: SelectionType.WholeBlock,
75
- blockId: selectionStart.clientId,
93
+ blockPosition,
76
94
  };
77
95
  } else if ( isCursorOnly ) {
78
96
  // Case 3: Cursor only, no text selected
@@ -85,7 +103,6 @@ export function getSelectionState(
85
103
 
86
104
  return {
87
105
  type: SelectionType.Cursor,
88
- blockId: selectionStart.clientId,
89
106
  cursorPosition,
90
107
  };
91
108
  } else if ( isSelectionInOneBlock ) {
@@ -103,13 +120,12 @@ export function getSelectionState(
103
120
 
104
121
  return {
105
122
  type: SelectionType.SelectionInOneBlock,
106
- blockId: selectionStart.clientId,
107
123
  cursorStartPosition,
108
124
  cursorEndPosition,
109
125
  };
110
126
  }
111
127
 
112
- // Caes 5: Selection in multiple blocks
128
+ // Case 5: Selection in multiple blocks
113
129
  const cursorStartPosition = getCursorPosition( selectionStart, yBlocks );
114
130
  const cursorEndPosition = getCursorPosition( selectionEnd, yBlocks );
115
131
  if ( ! cursorStartPosition || ! cursorEndPosition ) {
@@ -119,8 +135,6 @@ export function getSelectionState(
119
135
 
120
136
  return {
121
137
  type: SelectionType.SelectionInMultipleBlocks,
122
- blockStartId: selectionStart.clientId,
123
- blockEndId: selectionEnd.clientId,
124
138
  cursorStartPosition,
125
139
  cursorEndPosition,
126
140
  };
@@ -137,7 +151,8 @@ function getCursorPosition(
137
151
  selection: WPBlockSelection,
138
152
  blocks: YBlocks
139
153
  ): CursorPosition | null {
140
- const block = findBlockByClientId( selection.clientId, blocks );
154
+ const path = getBlockPathForLocalClientId( selection.clientId );
155
+ const block = path ? findBlockByPath( path, blocks ) : null;
141
156
  if (
142
157
  ! block ||
143
158
  ! selection.attributeKey ||
@@ -147,7 +162,12 @@ function getCursorPosition(
147
162
  }
148
163
 
149
164
  const attributes = block.get( 'attributes' );
150
- const currentYText = attributes?.get( selection.attributeKey ) as Y.Text;
165
+ const currentYText = attributes?.get( selection.attributeKey );
166
+
167
+ // If the attribute is not a Y.Text, return null.
168
+ if ( ! ( currentYText instanceof Y.Text ) ) {
169
+ return null;
170
+ }
151
171
 
152
172
  const relativePosition = Y.createRelativePositionFromTypeIndex(
153
173
  currentYText,
@@ -161,32 +181,103 @@ function getCursorPosition(
161
181
  }
162
182
 
163
183
  /**
164
- * Find a block by its client ID.
184
+ * Resolves a local block-editor clientId to its index path relative to the
185
+ * post content blocks. This allows finding the corresponding block in the Yjs
186
+ * document even when clientIds differ (e.g. in "Show Template" mode where
187
+ * blocks are cloned).
188
+ *
189
+ * In template mode, the block tree includes template parts and wrapper blocks
190
+ * around a core/post-content block. The Yjs document only contains the post
191
+ * content blocks, so we stop the upward walk when the parent is
192
+ * core/post-content (its inner blocks correspond to the Yjs root blocks).
193
+ *
194
+ * @param clientId - The local block-editor clientId to resolve.
195
+ * @return The index path from root, or null if not resolvable.
196
+ */
197
+ export function getBlockPathForLocalClientId(
198
+ clientId: string
199
+ ): AbsoluteBlockIndexPath | null {
200
+ const { getBlockIndex, getBlockRootClientId, getBlockName } =
201
+ select( blockEditorStore );
202
+
203
+ const path: AbsoluteBlockIndexPath = [];
204
+ let current: string | null = clientId;
205
+ while ( current ) {
206
+ const index = getBlockIndex( current );
207
+ if ( index === -1 ) {
208
+ return null;
209
+ }
210
+ path.unshift( index );
211
+ const parent = getBlockRootClientId( current );
212
+ if ( ! parent ) {
213
+ break;
214
+ }
215
+ // If the parent is core/post-content, stop here — the Yjs doc
216
+ // root blocks correspond to post-content's inner blocks.
217
+ const parentName = getBlockName( parent );
218
+ if ( parentName === 'core/post-content' ) {
219
+ break;
220
+ }
221
+ current = parent;
222
+ }
223
+ return path.length > 0 ? path : null;
224
+ }
225
+
226
+ /**
227
+ * Find a block by navigating a tree index path in the Yjs block hierarchy.
165
228
  *
166
- * @param blockId - The client ID of the block.
167
- * @param blocks - The blocks to search through.
168
- * @return The block if found, null otherwise.
229
+ * @param path - The index path, e.g. [0, 1] for blocks[0].innerBlocks[1].
230
+ * @param blocks - The root-level Yjs blocks array.
231
+ * @return The block Y.Map if found, null otherwise.
169
232
  */
170
- function findBlockByClientId(
171
- blockId: string,
233
+ function findBlockByPath(
234
+ path: AbsoluteBlockIndexPath,
172
235
  blocks: YBlocks
173
236
  ): YBlock | null {
174
- for ( const block of blocks ) {
175
- if ( block.get( 'clientId' ) === blockId ) {
237
+ let currentBlocks = blocks;
238
+ for ( let i = 0; i < path.length; i++ ) {
239
+ if ( path[ i ] >= currentBlocks.length ) {
240
+ return null;
241
+ }
242
+ const block = currentBlocks.get( path[ i ] );
243
+ if ( ! block ) {
244
+ return null;
245
+ }
246
+ if ( i === path.length - 1 ) {
176
247
  return block;
177
248
  }
249
+ currentBlocks =
250
+ block.get( 'innerBlocks' ) ?? ( new Y.Array() as YBlocks );
251
+ }
252
+ return null;
253
+ }
178
254
 
179
- const innerBlocks = block.get( 'innerBlocks' );
180
-
181
- if ( innerBlocks && innerBlocks.length > 0 ) {
182
- const innerBlock = findBlockByClientId( blockId, innerBlocks );
183
-
184
- if ( innerBlock ) {
185
- return innerBlock;
186
- }
255
+ /**
256
+ * Create a Y.RelativePosition for a block by navigating a tree index path.
257
+ *
258
+ * @param path - The index path, e.g. [0, 1] for blocks[0].innerBlocks[1].
259
+ * @param blocks - The root-level Yjs blocks array.
260
+ * @return A Y.RelativePosition for the block, or null if the path is invalid.
261
+ */
262
+ function createRelativePositionForBlockPath(
263
+ path: AbsoluteBlockIndexPath,
264
+ blocks: YBlocks
265
+ ): Y.RelativePosition | null {
266
+ let currentBlocks = blocks;
267
+ for ( let i = 0; i < path.length; i++ ) {
268
+ if ( path[ i ] >= currentBlocks.length ) {
269
+ return null;
270
+ }
271
+ if ( i === path.length - 1 ) {
272
+ return Y.createRelativePositionFromTypeIndex(
273
+ currentBlocks,
274
+ path[ i ]
275
+ );
187
276
  }
277
+ const block = currentBlocks.get( path[ i ] );
278
+ currentBlocks =
279
+ block?.get( 'innerBlocks' ) ?? ( new Y.Array() as YBlocks );
188
280
  }
189
-
190
281
  return null;
191
282
  }
192
283
 
@@ -210,19 +301,13 @@ export function areSelectionsStatesEqual(
210
301
  return true;
211
302
 
212
303
  case SelectionType.Cursor:
213
- return (
214
- selection1.blockId ===
215
- ( selection2 as SelectionCursor ).blockId &&
216
- areCursorPositionsEqual(
217
- selection1.cursorPosition,
218
- ( selection2 as SelectionCursor ).cursorPosition
219
- )
304
+ return areCursorPositionsEqual(
305
+ selection1.cursorPosition,
306
+ ( selection2 as SelectionCursor ).cursorPosition
220
307
  );
221
308
 
222
309
  case SelectionType.SelectionInOneBlock:
223
310
  return (
224
- selection1.blockId ===
225
- ( selection2 as SelectionInOneBlock ).blockId &&
226
311
  areCursorPositionsEqual(
227
312
  selection1.cursorStartPosition,
228
313
  ( selection2 as SelectionInOneBlock ).cursorStartPosition
@@ -235,10 +320,6 @@ export function areSelectionsStatesEqual(
235
320
 
236
321
  case SelectionType.SelectionInMultipleBlocks:
237
322
  return (
238
- selection1.blockStartId ===
239
- ( selection2 as SelectionInMultipleBlocks ).blockStartId &&
240
- selection1.blockEndId ===
241
- ( selection2 as SelectionInMultipleBlocks ).blockEndId &&
242
323
  areCursorPositionsEqual(
243
324
  selection1.cursorStartPosition,
244
325
  ( selection2 as SelectionInMultipleBlocks )
@@ -251,9 +332,9 @@ export function areSelectionsStatesEqual(
251
332
  )
252
333
  );
253
334
  case SelectionType.WholeBlock:
254
- return (
255
- selection1.blockId ===
256
- ( selection2 as SelectionWholeBlock ).blockId
335
+ return Y.compareRelativePositions(
336
+ selection1.blockPosition,
337
+ ( selection2 as SelectionWholeBlock ).blockPosition
257
338
  );
258
339
 
259
340
  default:
@@ -272,9 +353,10 @@ function areCursorPositionsEqual(
272
353
  cursorPosition1: CursorPosition,
273
354
  cursorPosition2: CursorPosition
274
355
  ): boolean {
275
- const isRelativePositionEqual =
276
- JSON.stringify( cursorPosition1.relativePosition ) ===
277
- JSON.stringify( cursorPosition2.relativePosition );
356
+ const isRelativePositionEqual = Y.compareRelativePositions(
357
+ cursorPosition1.relativePosition,
358
+ cursorPosition2.relativePosition
359
+ );
278
360
 
279
361
  // Ensure a change in calculated absolute offset results in a treating the cursor as modified.
280
362
  // This is necessary because Y.Text relative positions can remain the same after text changes.
package/src/utils/crdt.ts CHANGED
@@ -28,13 +28,13 @@ import {
28
28
  } from './crdt-blocks';
29
29
  import { type Post } from '../entity-types/post';
30
30
  import { type Type } from '../entity-types';
31
- import {
32
- CRDT_DOC_META_PERSISTENCE_KEY,
33
- CRDT_RECORD_MAP_KEY,
34
- WORDPRESS_META_KEY_FOR_CRDT_DOC_PERSISTENCE,
35
- } from '../sync';
31
+ import { CRDT_DOC_META_PERSISTENCE_KEY, CRDT_RECORD_MAP_KEY } from '../sync';
36
32
  import type { WPSelection } from '../types';
37
- import { updateSelectionHistory } from './crdt-selection';
33
+ import {
34
+ getSelectionHistory,
35
+ getShiftedSelection,
36
+ updateSelectionHistory,
37
+ } from './crdt-selection';
38
38
  import {
39
39
  createYMap,
40
40
  getRootMap,
@@ -74,6 +74,8 @@ export interface YPostRecord extends YMapRecord {
74
74
  title: Y.Text;
75
75
  }
76
76
 
77
+ export const POST_META_KEY_FOR_CRDT_DOC_PERSISTENCE = '_crdt_document';
78
+
77
79
  // Properties that are allowed to be synced for a post.
78
80
  const allowedPostProperties = new Set< string >( [
79
81
  'author',
@@ -97,7 +99,7 @@ const allowedPostProperties = new Set< string >( [
97
99
 
98
100
  // Post meta keys that should *not* be synced.
99
101
  const disallowedPostMetaKeys = new Set< string >( [
100
- WORDPRESS_META_KEY_FOR_CRDT_DOC_PERSISTENCE,
102
+ POST_META_KEY_FOR_CRDT_DOC_PERSISTENCE,
101
103
  ] );
102
104
 
103
105
  /**
@@ -416,6 +418,20 @@ export function getPostChangesFromCRDTDoc(
416
418
  };
417
419
  }
418
420
 
421
+ // When remote content changes are detected, recalculate the local user's
422
+ // selection using Y.RelativePosition to account for text shifts. The ydoc
423
+ // has already been updated with remote content at this point, so converting
424
+ // relative positions to absolute gives corrected offsets. Including the
425
+ // selection in PostChanges ensures it dispatches atomically with content.
426
+ const selectionHistory = getSelectionHistory( ydoc );
427
+ const shiftedSelection = getShiftedSelection( ydoc, selectionHistory );
428
+ if ( shiftedSelection ) {
429
+ changes.selection = {
430
+ ...shiftedSelection,
431
+ initialPosition: 0,
432
+ };
433
+ }
434
+
419
435
  return changes;
420
436
  }
421
437