@wordpress/core-data 7.40.2-next.v.202602241322.0 → 7.40.2

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 (183) hide show
  1. package/README.md +0 -27
  2. package/build/actions.cjs +9 -26
  3. package/build/actions.cjs.map +2 -2
  4. package/build/awareness/post-editor-awareness.cjs +45 -6
  5. package/build/awareness/post-editor-awareness.cjs.map +2 -2
  6. package/build/awareness/types.cjs.map +1 -1
  7. package/build/entities.cjs +50 -23
  8. package/build/entities.cjs.map +2 -2
  9. package/build/entity-provider.cjs +15 -6
  10. package/build/entity-provider.cjs.map +2 -2
  11. package/build/hooks/use-entity-prop.cjs +33 -2
  12. package/build/hooks/use-entity-prop.cjs.map +2 -2
  13. package/build/hooks/use-post-editor-awareness-state.cjs +120 -1
  14. package/build/hooks/use-post-editor-awareness-state.cjs.map +2 -2
  15. package/build/index.cjs +0 -3
  16. package/build/index.cjs.map +2 -2
  17. package/build/private-actions.cjs +27 -3
  18. package/build/private-actions.cjs.map +2 -2
  19. package/build/private-apis.cjs +14 -5
  20. package/build/private-apis.cjs.map +3 -3
  21. package/build/private-selectors.cjs +22 -2
  22. package/build/private-selectors.cjs.map +2 -2
  23. package/build/queried-data/actions.cjs +1 -1
  24. package/build/queried-data/actions.cjs.map +2 -2
  25. package/build/queried-data/reducer.cjs +19 -13
  26. package/build/queried-data/reducer.cjs.map +2 -2
  27. package/build/reducer.cjs +19 -2
  28. package/build/reducer.cjs.map +2 -2
  29. package/build/resolvers.cjs +16 -6
  30. package/build/resolvers.cjs.map +2 -2
  31. package/build/selectors.cjs +0 -15
  32. package/build/selectors.cjs.map +2 -2
  33. package/build/sync.cjs +11 -5
  34. package/build/sync.cjs.map +2 -2
  35. package/build/types.cjs.map +1 -1
  36. package/build/utils/block-selection-history.cjs +1 -1
  37. package/build/utils/block-selection-history.cjs.map +2 -2
  38. package/build/utils/crdt-blocks.cjs +125 -49
  39. package/build/utils/crdt-blocks.cjs.map +2 -2
  40. package/build/utils/crdt-selection.cjs +50 -19
  41. package/build/utils/crdt-selection.cjs.map +2 -2
  42. package/build/utils/crdt-text.cjs +52 -0
  43. package/build/utils/crdt-text.cjs.map +7 -0
  44. package/build/utils/crdt-user-selections.cjs +16 -6
  45. package/build/utils/crdt-user-selections.cjs.map +3 -3
  46. package/build/utils/crdt-utils.cjs +54 -2
  47. package/build/utils/crdt-utils.cjs.map +2 -2
  48. package/build/utils/crdt.cjs +38 -29
  49. package/build/utils/crdt.cjs.map +2 -2
  50. package/build-module/actions.mjs +14 -26
  51. package/build-module/actions.mjs.map +2 -2
  52. package/build-module/awareness/post-editor-awareness.mjs +47 -7
  53. package/build-module/awareness/post-editor-awareness.mjs.map +2 -2
  54. package/build-module/entities.mjs +53 -24
  55. package/build-module/entities.mjs.map +2 -2
  56. package/build-module/entity-provider.mjs +15 -6
  57. package/build-module/entity-provider.mjs.map +2 -2
  58. package/build-module/hooks/use-entity-prop.mjs +34 -3
  59. package/build-module/hooks/use-entity-prop.mjs.map +2 -2
  60. package/build-module/hooks/use-post-editor-awareness-state.mjs +117 -1
  61. package/build-module/hooks/use-post-editor-awareness-state.mjs.map +2 -2
  62. package/build-module/index.mjs +0 -2
  63. package/build-module/index.mjs.map +2 -2
  64. package/build-module/private-actions.mjs +24 -2
  65. package/build-module/private-actions.mjs.map +2 -2
  66. package/build-module/private-apis.mjs +21 -6
  67. package/build-module/private-apis.mjs.map +3 -3
  68. package/build-module/private-selectors.mjs +19 -1
  69. package/build-module/private-selectors.mjs.map +2 -2
  70. package/build-module/queried-data/actions.mjs +1 -1
  71. package/build-module/queried-data/actions.mjs.map +2 -2
  72. package/build-module/queried-data/reducer.mjs +19 -13
  73. package/build-module/queried-data/reducer.mjs.map +2 -2
  74. package/build-module/reducer.mjs +18 -2
  75. package/build-module/reducer.mjs.map +2 -2
  76. package/build-module/resolvers.mjs +16 -6
  77. package/build-module/resolvers.mjs.map +2 -2
  78. package/build-module/selectors.mjs +0 -14
  79. package/build-module/selectors.mjs.map +2 -2
  80. package/build-module/sync.mjs +7 -3
  81. package/build-module/sync.mjs.map +2 -2
  82. package/build-module/utils/block-selection-history.mjs +5 -2
  83. package/build-module/utils/block-selection-history.mjs.map +2 -2
  84. package/build-module/utils/crdt-blocks.mjs +124 -49
  85. package/build-module/utils/crdt-blocks.mjs.map +2 -2
  86. package/build-module/utils/crdt-selection.mjs +53 -20
  87. package/build-module/utils/crdt-selection.mjs.map +2 -2
  88. package/build-module/utils/crdt-text.mjs +26 -0
  89. package/build-module/utils/crdt-text.mjs.map +7 -0
  90. package/build-module/utils/crdt-user-selections.mjs +16 -7
  91. package/build-module/utils/crdt-user-selections.mjs.map +3 -3
  92. package/build-module/utils/crdt-utils.mjs +51 -1
  93. package/build-module/utils/crdt-utils.mjs.map +2 -2
  94. package/build-module/utils/crdt.mjs +42 -34
  95. package/build-module/utils/crdt.mjs.map +2 -2
  96. package/build-types/actions.d.ts +0 -11
  97. package/build-types/actions.d.ts.map +1 -1
  98. package/build-types/awareness/post-editor-awareness.d.ts +2 -2
  99. package/build-types/awareness/post-editor-awareness.d.ts.map +1 -1
  100. package/build-types/awareness/types.d.ts +6 -1
  101. package/build-types/awareness/types.d.ts.map +1 -1
  102. package/build-types/entities.d.ts +20 -1
  103. package/build-types/entities.d.ts.map +1 -1
  104. package/build-types/entity-provider.d.ts +11 -6
  105. package/build-types/entity-provider.d.ts.map +1 -1
  106. package/build-types/hooks/use-entity-prop.d.ts.map +1 -1
  107. package/build-types/hooks/use-post-editor-awareness-state.d.ts +40 -7
  108. package/build-types/hooks/use-post-editor-awareness-state.d.ts.map +1 -1
  109. package/build-types/index.d.ts +0 -3
  110. package/build-types/index.d.ts.map +1 -1
  111. package/build-types/parsed-blocks-cache.d.ts +10 -0
  112. package/build-types/parsed-blocks-cache.d.ts.map +1 -0
  113. package/build-types/private-actions.d.ts +12 -0
  114. package/build-types/private-actions.d.ts.map +1 -1
  115. package/build-types/private-apis.d.ts +21 -1
  116. package/build-types/private-apis.d.ts.map +1 -1
  117. package/build-types/private-selectors.d.ts +17 -0
  118. package/build-types/private-selectors.d.ts.map +1 -1
  119. package/build-types/queried-data/reducer.d.ts.map +1 -1
  120. package/build-types/reducer.d.ts +15 -0
  121. package/build-types/reducer.d.ts.map +1 -1
  122. package/build-types/resolvers.d.ts.map +1 -1
  123. package/build-types/selectors.d.ts +1 -9
  124. package/build-types/selectors.d.ts.map +1 -1
  125. package/build-types/sync.d.ts +2 -2
  126. package/build-types/sync.d.ts.map +1 -1
  127. package/build-types/types.d.ts +11 -2
  128. package/build-types/types.d.ts.map +1 -1
  129. package/build-types/utils/block-selection-history.d.ts.map +1 -1
  130. package/build-types/utils/crdt-blocks.d.ts +12 -1
  131. package/build-types/utils/crdt-blocks.d.ts.map +1 -1
  132. package/build-types/utils/crdt-selection.d.ts +10 -0
  133. package/build-types/utils/crdt-selection.d.ts.map +1 -1
  134. package/build-types/utils/crdt-text.d.ts +16 -0
  135. package/build-types/utils/crdt-text.d.ts.map +1 -0
  136. package/build-types/utils/crdt-user-selections.d.ts +17 -4
  137. package/build-types/utils/crdt-user-selections.d.ts.map +1 -1
  138. package/build-types/utils/crdt-utils.d.ts +20 -0
  139. package/build-types/utils/crdt-utils.d.ts.map +1 -1
  140. package/build-types/utils/crdt.d.ts +14 -7
  141. package/build-types/utils/crdt.d.ts.map +1 -1
  142. package/build-types/utils/normalize-query-for-resolution.d.ts +12 -0
  143. package/build-types/utils/normalize-query-for-resolution.d.ts.map +1 -0
  144. package/build-types/utils/save-crdt-doc.d.ts +8 -0
  145. package/build-types/utils/save-crdt-doc.d.ts.map +1 -0
  146. package/build-types/utils/test/crdt-utils.d.ts +2 -0
  147. package/build-types/utils/test/crdt-utils.d.ts.map +1 -0
  148. package/package.json +18 -18
  149. package/src/actions.js +26 -41
  150. package/src/awareness/post-editor-awareness.ts +106 -7
  151. package/src/awareness/test/post-editor-awareness.ts +50 -10
  152. package/src/awareness/types.ts +7 -1
  153. package/src/entities.js +62 -24
  154. package/src/entity-provider.js +24 -11
  155. package/src/hooks/test/use-post-editor-awareness-state.ts +446 -3
  156. package/src/hooks/use-entity-prop.js +43 -3
  157. package/src/hooks/use-post-editor-awareness-state.ts +229 -7
  158. package/src/index.js +0 -6
  159. package/src/private-actions.js +43 -1
  160. package/src/{private-apis.js → private-apis.ts} +21 -3
  161. package/src/private-selectors.ts +43 -0
  162. package/src/queried-data/actions.js +1 -1
  163. package/src/queried-data/reducer.js +26 -14
  164. package/src/reducer.js +36 -1
  165. package/src/resolvers.js +23 -7
  166. package/src/selectors.ts +1 -32
  167. package/src/sync.ts +6 -2
  168. package/src/test/entities.js +231 -14
  169. package/src/test/private-actions.js +1 -1
  170. package/src/test/resolvers.js +64 -11
  171. package/src/test/store.js +116 -0
  172. package/src/types.ts +21 -2
  173. package/src/utils/block-selection-history.ts +5 -2
  174. package/src/utils/crdt-blocks.ts +250 -81
  175. package/src/utils/crdt-selection.ts +92 -26
  176. package/src/utils/crdt-text.ts +43 -0
  177. package/src/utils/crdt-user-selections.ts +29 -8
  178. package/src/utils/crdt-utils.ts +99 -0
  179. package/src/utils/crdt.ts +72 -46
  180. package/src/utils/test/crdt-blocks.ts +1177 -22
  181. package/src/utils/test/crdt-user-selections.ts +5 -0
  182. package/src/utils/test/crdt-utils.ts +387 -0
  183. package/src/utils/test/crdt.ts +423 -60
@@ -12,7 +12,7 @@ import { store as blockEditorStore } from '@wordpress/block-editor';
12
12
  import { CRDT_RECORD_MAP_KEY } from '../sync';
13
13
  import type { YPostRecord } from './crdt';
14
14
  import type { YBlock, YBlocks } from './crdt-blocks';
15
- import { getRootMap } from './crdt-utils';
15
+ import { getRootMap, richTextOffsetToHtmlIndex } from './crdt-utils';
16
16
  import type {
17
17
  AbsoluteBlockIndexPath,
18
18
  WPBlockSelection,
@@ -36,6 +36,16 @@ export enum SelectionType {
36
36
  WholeBlock = 'whole-block',
37
37
  }
38
38
 
39
+ /**
40
+ * The direction of a text selection, indicating where the caret sits.
41
+ */
42
+ export enum SelectionDirection {
43
+ /** The caret is at the end of the selection (default / left-to-right). */
44
+ Forward = 'f',
45
+ /** The caret is at the start of the selection (right-to-left). */
46
+ Backward = 'b',
47
+ }
48
+
39
49
  /**
40
50
  * Converts WordPress block editor selection to a SelectionState.
41
51
  *
@@ -44,16 +54,20 @@ export enum SelectionType {
44
54
  * differ between the block-editor store and the Yjs document (e.g. in "Show
45
55
  * Template" mode).
46
56
  *
47
- * @param selectionStart - The start position of the selection
48
- * @param selectionEnd - The end position of the selection
49
- * @param yDoc - The Yjs document
57
+ * @param selectionStart - The start position of the selection
58
+ * @param selectionEnd - The end position of the selection
59
+ * @param yDoc - The Yjs document
60
+ * @param options - Optional parameters
61
+ * @param options.selectionDirection - The direction of the selection (forward or backward)
50
62
  * @return The SelectionState
51
63
  */
52
64
  export function getSelectionState(
53
65
  selectionStart: WPBlockSelection,
54
66
  selectionEnd: WPBlockSelection,
55
- yDoc: Y.Doc
67
+ yDoc: Y.Doc,
68
+ options?: { selectionDirection?: SelectionDirection }
56
69
  ): SelectionState {
70
+ const { selectionDirection } = options ?? {};
57
71
  const ymap = getRootMap< YPostRecord >( yDoc, CRDT_RECORD_MAP_KEY );
58
72
  const yBlocks = ymap.get( 'blocks' );
59
73
 
@@ -122,6 +136,7 @@ export function getSelectionState(
122
136
  type: SelectionType.SelectionInOneBlock,
123
137
  cursorStartPosition,
124
138
  cursorEndPosition,
139
+ selectionDirection,
125
140
  };
126
141
  }
127
142
 
@@ -137,6 +152,7 @@ export function getSelectionState(
137
152
  type: SelectionType.SelectionInMultipleBlocks,
138
153
  cursorStartPosition,
139
154
  cursorEndPosition,
155
+ selectionDirection,
140
156
  };
141
157
  }
142
158
 
@@ -171,7 +187,7 @@ function getCursorPosition(
171
187
 
172
188
  const relativePosition = Y.createRelativePositionFromTypeIndex(
173
189
  currentYText,
174
- selection.offset
190
+ richTextOffsetToHtmlIndex( currentYText.toString(), selection.offset )
175
191
  );
176
192
 
177
193
  return {
@@ -315,7 +331,9 @@ export function areSelectionsStatesEqual(
315
331
  areCursorPositionsEqual(
316
332
  selection1.cursorEndPosition,
317
333
  ( selection2 as SelectionInOneBlock ).cursorEndPosition
318
- )
334
+ ) &&
335
+ selection1.selectionDirection ===
336
+ ( selection2 as SelectionInOneBlock ).selectionDirection
319
337
  );
320
338
 
321
339
  case SelectionType.SelectionInMultipleBlocks:
@@ -329,7 +347,10 @@ export function areSelectionsStatesEqual(
329
347
  selection1.cursorEndPosition,
330
348
  ( selection2 as SelectionInMultipleBlocks )
331
349
  .cursorEndPosition
332
- )
350
+ ) &&
351
+ selection1.selectionDirection ===
352
+ ( selection2 as SelectionInMultipleBlocks )
353
+ .selectionDirection
333
354
  );
334
355
  case SelectionType.WholeBlock:
335
356
  return Y.compareRelativePositions(
@@ -2,6 +2,7 @@
2
2
  * WordPress dependencies
3
3
  */
4
4
  import { Y } from '@wordpress/sync';
5
+ import { create, insert, toHTMLString } from '@wordpress/rich-text';
5
6
 
6
7
  /**
7
8
  * Internal dependencies
@@ -104,6 +105,104 @@ export function findBlockByClientIdInDoc(
104
105
  return findBlockByClientIdInBlocks( blockId, blocks );
105
106
  }
106
107
 
108
+ // Marker for insertion.
109
+ const MARKER_START = 0xe000;
110
+
111
+ /**
112
+ * Pick a marker character that does not appear in `text`. Returns the marker
113
+ * or `null` if all candidates are present (extremely unlikely in practice).
114
+ *
115
+ * @param text The string to check for existing marker characters.
116
+ */
117
+ function pickMarker( text: string ): string | null {
118
+ const tryCount = 0x10;
119
+
120
+ // Scan the unicode private use area for the first code point not present
121
+ // in the text.
122
+ for ( let code = MARKER_START; code < MARKER_START + tryCount; code++ ) {
123
+ const candidate = String.fromCharCode( code );
124
+
125
+ if ( ! text.includes( candidate ) ) {
126
+ return candidate;
127
+ }
128
+ }
129
+
130
+ return null;
131
+ }
132
+
133
+ /**
134
+ * Convert an HTML character index (counting tag characters) to a rich-text
135
+ * offset (counting only text characters). Used on read paths where Y.Text
136
+ * resolves to an HTML index but the block editor expects a text offset.
137
+ *
138
+ * @param html The full HTML string from Y.Text.
139
+ * @param htmlIndex The HTML character index.
140
+ * @return The corresponding rich-text offset.
141
+ */
142
+ export function htmlIndexToRichTextOffset(
143
+ html: string,
144
+ htmlIndex: number
145
+ ): number {
146
+ if ( ! html.includes( '<' ) && ! html.includes( '&' ) ) {
147
+ return htmlIndex;
148
+ }
149
+
150
+ const marker = pickMarker( html );
151
+ if ( ! marker ) {
152
+ return htmlIndex;
153
+ }
154
+
155
+ // Insert marker and let create() do the parsing.
156
+ const withMarker =
157
+ html.slice( 0, htmlIndex ) + marker + html.slice( htmlIndex );
158
+ const value = create( { html: withMarker } );
159
+ const markerPos = value.text.indexOf( marker );
160
+
161
+ return markerPos === -1 ? htmlIndex : markerPos;
162
+ }
163
+
164
+ /**
165
+ * Convert a rich-text offset (counting only text characters) to an HTML
166
+ * character index (counting tag characters). Used on write paths where the
167
+ * block editor provides a text offset but Y.Text expects an HTML index.
168
+ *
169
+ * @param html The full HTML string from Y.Text.
170
+ * @param richTextOffset The rich-text text offset.
171
+ * @return The corresponding HTML character index.
172
+ */
173
+ export function richTextOffsetToHtmlIndex(
174
+ html: string,
175
+ richTextOffset: number
176
+ ): number {
177
+ if ( ! html.includes( '<' ) && ! html.includes( '&' ) ) {
178
+ return richTextOffset;
179
+ }
180
+
181
+ const marker = pickMarker( html );
182
+ if ( ! marker ) {
183
+ return richTextOffset;
184
+ }
185
+
186
+ const value = create( { html } );
187
+ const markerValue = create( { text: marker } );
188
+ // The marker must inherit the formatting at the insertion point so that
189
+ // toHTMLString does not split surrounding tags (e.g. <strong>) around it.
190
+ if ( value.formats[ richTextOffset ] ) {
191
+ markerValue.formats[ 0 ] = value.formats[ richTextOffset ];
192
+ }
193
+
194
+ const withMarker = insert(
195
+ value,
196
+ markerValue,
197
+ richTextOffset,
198
+ richTextOffset
199
+ );
200
+
201
+ const htmlWithMarker = toHTMLString( { value: withMarker } );
202
+ const markerIndex = htmlWithMarker.indexOf( marker );
203
+ return markerIndex === -1 ? richTextOffset : markerIndex;
204
+ }
205
+
107
206
  function findBlockByClientIdInBlocks(
108
207
  blockId: string,
109
208
  blocks: YBlocks
package/src/utils/crdt.ts CHANGED
@@ -11,6 +11,8 @@ import { __unstableSerializeAndClean } from '@wordpress/blocks';
11
11
  import {
12
12
  type CRDTDoc,
13
13
  type ObjectData,
14
+ type ObjectID,
15
+ type ObjectType,
14
16
  type SyncConfig,
15
17
  Y,
16
18
  } from '@wordpress/sync';
@@ -20,6 +22,7 @@ import {
20
22
  */
21
23
  import { BaseAwareness } from '../awareness/base-awareness';
22
24
  import {
25
+ deserializeBlockAttributes,
23
26
  mergeCrdtBlocks,
24
27
  mergeRichTextUpdate,
25
28
  type Block,
@@ -27,14 +30,13 @@ import {
27
30
  type YBlocks,
28
31
  } from './crdt-blocks';
29
32
  import { type Post } from '../entity-types/post';
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';
33
+ import { CRDT_DOC_META_PERSISTENCE_KEY, CRDT_RECORD_MAP_KEY } from '../sync';
36
34
  import type { WPSelection } from '../types';
37
- import { updateSelectionHistory } from './crdt-selection';
35
+ import {
36
+ getSelectionHistory,
37
+ getShiftedSelection,
38
+ updateSelectionHistory,
39
+ } from './crdt-selection';
38
40
  import {
39
41
  createYMap,
40
42
  getRootMap,
@@ -74,30 +76,11 @@ export interface YPostRecord extends YMapRecord {
74
76
  title: Y.Text;
75
77
  }
76
78
 
77
- // Properties that are allowed to be synced for a post.
78
- const allowedPostProperties = new Set< string >( [
79
- 'author',
80
- 'blocks',
81
- 'content',
82
- 'categories',
83
- 'comment_status',
84
- 'date',
85
- 'excerpt',
86
- 'featured_media',
87
- 'format',
88
- 'meta',
89
- 'ping_status',
90
- 'slug',
91
- 'status',
92
- 'sticky',
93
- 'tags',
94
- 'template',
95
- 'title',
96
- ] );
79
+ export const POST_META_KEY_FOR_CRDT_DOC_PERSISTENCE = '_crdt_document';
97
80
 
98
81
  // Post meta keys that should *not* be synced.
99
82
  const disallowedPostMetaKeys = new Set< string >( [
100
- WORDPRESS_META_KEY_FOR_CRDT_DOC_PERSISTENCE,
83
+ POST_META_KEY_FOR_CRDT_DOC_PERSISTENCE,
101
84
  ] );
102
85
 
103
86
  /**
@@ -137,18 +120,18 @@ function defaultApplyChangesToCRDTDoc(
137
120
  *
138
121
  * @param {CRDTDoc} ydoc
139
122
  * @param {PostChanges} changes
140
- * @param {Type} _postType
123
+ * @param {Set<string>} syncedProperties
141
124
  * @return {void}
142
125
  */
143
126
  export function applyPostChangesToCRDTDoc(
144
127
  ydoc: CRDTDoc,
145
128
  changes: PostChanges,
146
- _postType: Type // eslint-disable-line @typescript-eslint/no-unused-vars
129
+ syncedProperties: Set< string >
147
130
  ): void {
148
131
  const ymap = getRootMap< YPostRecord >( ydoc, CRDT_RECORD_MAP_KEY );
149
132
 
150
133
  Object.keys( changes ).forEach( ( key ) => {
151
- if ( ! allowedPostProperties.has( key ) ) {
134
+ if ( ! syncedProperties.has( key ) ) {
152
135
  return;
153
136
  }
154
137
 
@@ -287,15 +270,15 @@ function defaultGetChangesFromCRDTDoc( crdtDoc: CRDTDoc ): ObjectData {
287
270
  * against the local record and determine if there are changes (edits) we want
288
271
  * to dispatch.
289
272
  *
290
- * @param {CRDTDoc} ydoc
291
- * @param {Post} editedRecord
292
- * @param {Type} _postType
273
+ * @param {CRDTDoc} ydoc
274
+ * @param {Post} editedRecord
275
+ * @param {Set<string>} syncedProperties
293
276
  * @return {Partial<PostChanges>} The changes that should be applied to the local record.
294
277
  */
295
278
  export function getPostChangesFromCRDTDoc(
296
279
  ydoc: CRDTDoc,
297
280
  editedRecord: Post,
298
- _postType: Type // eslint-disable-line @typescript-eslint/no-unused-vars
281
+ syncedProperties: Set< string >
299
282
  ): PostChanges {
300
283
  const ymap = getRootMap< YPostRecord >( ydoc, CRDT_RECORD_MAP_KEY );
301
284
 
@@ -303,7 +286,7 @@ export function getPostChangesFromCRDTDoc(
303
286
 
304
287
  const changes = Object.fromEntries(
305
288
  Object.entries( ymap.toJSON() ).filter( ( [ key, newValue ] ) => {
306
- if ( ! allowedPostProperties.has( key ) ) {
289
+ if ( ! syncedProperties.has( key ) ) {
307
290
  return false;
308
291
  }
309
292
 
@@ -340,8 +323,6 @@ export function getPostChangesFromCRDTDoc(
340
323
  );
341
324
  }
342
325
 
343
- // The consumers of blocks have memoization that renders optimization
344
- // here unnecessary.
345
326
  return true;
346
327
  }
347
328
 
@@ -349,11 +330,8 @@ export function getPostChangesFromCRDTDoc(
349
330
  // Do not overwrite a "floating" date. Borrowing logic from the
350
331
  // isEditedPostDateFloating selector.
351
332
  const currentDateIsFloating =
352
- [ 'draft', 'auto-draft', 'pending' ].includes(
353
- ymap.get( 'status' ) as string
354
- ) &&
355
- ( null === currentValue ||
356
- editedRecord.modified === currentValue );
333
+ null === currentValue ||
334
+ editedRecord.modified === currentValue;
357
335
 
358
336
  if ( currentDateIsFloating ) {
359
337
  return false;
@@ -363,17 +341,29 @@ export function getPostChangesFromCRDTDoc(
363
341
  }
364
342
 
365
343
  case 'meta': {
344
+ const currentMeta =
345
+ ( currentValue as PostChanges[ 'meta' ] ) ?? {};
346
+
366
347
  allowedMetaChanges = Object.fromEntries(
367
348
  Object.entries( newValue ?? {} ).filter(
368
- ( [ metaKey ] ) =>
369
- ! disallowedPostMetaKeys.has( metaKey )
349
+ ( [ metaKey ] ) => {
350
+ if ( disallowedPostMetaKeys.has( metaKey ) ) {
351
+ return false;
352
+ }
353
+
354
+ // Ignore meta keys that are no longer registered
355
+ // for this post (absent from the REST response).
356
+ // Without this, orphaned CRDT meta would mark
357
+ // the post permanently dirty.
358
+ return metaKey in currentMeta;
359
+ }
370
360
  )
371
361
  );
372
362
 
373
363
  // Merge the allowed meta changes with the current meta values since
374
364
  // not all meta properties are synced.
375
365
  const mergedValue = {
376
- ...( currentValue as PostChanges[ 'meta' ] ),
366
+ ...currentMeta,
377
367
  ...allowedMetaChanges,
378
368
  };
379
369
 
@@ -407,6 +397,15 @@ export function getPostChangesFromCRDTDoc(
407
397
  } )
408
398
  );
409
399
 
400
+ // Blocks extracted from the CRDT document have rich-text attributes as
401
+ // plain strings (from Y.Text.toJSON()). Convert them back to RichTextData
402
+ // so block edit components receive the same types as locally-created blocks.
403
+ if ( changes.blocks ) {
404
+ changes.blocks = deserializeBlockAttributes(
405
+ changes.blocks as Block[]
406
+ );
407
+ }
408
+
410
409
  // Meta changes must be merged with the edited record since not all meta
411
410
  // properties are synced.
412
411
  if ( 'object' === typeof changes.meta ) {
@@ -416,6 +415,20 @@ export function getPostChangesFromCRDTDoc(
416
415
  };
417
416
  }
418
417
 
418
+ // When remote content changes are detected, recalculate the local user's
419
+ // selection using Y.RelativePosition to account for text shifts. The ydoc
420
+ // has already been updated with remote content at this point, so converting
421
+ // relative positions to absolute gives corrected offsets. Including the
422
+ // selection in PostChanges ensures it dispatches atomically with content.
423
+ const selectionHistory = getSelectionHistory( ydoc );
424
+ const shiftedSelection = getShiftedSelection( ydoc, selectionHistory );
425
+ if ( shiftedSelection ) {
426
+ changes.selection = {
427
+ ...shiftedSelection,
428
+ initialPosition: 0,
429
+ };
430
+ }
431
+
419
432
  return changes;
420
433
  }
421
434
 
@@ -429,6 +442,19 @@ export const defaultSyncConfig: SyncConfig = {
429
442
  getChangesFromCRDTDoc: defaultGetChangesFromCRDTDoc,
430
443
  };
431
444
 
445
+ /**
446
+ * This default collection sync config can be used to sync entity collections
447
+ * (e.g., block comments) where we are not interested in merging changes at the
448
+ * individual record level, but instead want to replace the entire collection
449
+ * when changes are detected.
450
+ */
451
+ export const defaultCollectionSyncConfig: SyncConfig = {
452
+ applyChangesToCRDTDoc: () => {},
453
+ getChangesFromCRDTDoc: () => ( {} ),
454
+ shouldSync: ( _: ObjectType, objectId: ObjectID | null ) =>
455
+ null === objectId,
456
+ };
457
+
432
458
  /**
433
459
  * Extract the raw string value from a property that may be a string or an object
434
460
  * with a `raw` property (`RenderedText`).