@wordpress/core-data 7.38.1-next.v.0 → 7.39.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 (85) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/build/actions.cjs +17 -6
  3. package/build/actions.cjs.map +2 -2
  4. package/build/awareness/base-awareness.cjs +62 -0
  5. package/build/awareness/base-awareness.cjs.map +7 -0
  6. package/build/awareness/post-editor-awareness.cjs +4 -18
  7. package/build/awareness/post-editor-awareness.cjs.map +3 -3
  8. package/build/entities.cjs +14 -2
  9. package/build/entities.cjs.map +2 -2
  10. package/build/resolvers.cjs +40 -0
  11. package/build/resolvers.cjs.map +2 -2
  12. package/build/types.cjs.map +1 -1
  13. package/build/utils/block-selection-history.cjs +101 -0
  14. package/build/utils/block-selection-history.cjs.map +7 -0
  15. package/build/utils/crdt-selection.cjs +139 -0
  16. package/build/utils/crdt-selection.cjs.map +7 -0
  17. package/build/utils/crdt-user-selections.cjs +2 -2
  18. package/build/utils/crdt-user-selections.cjs.map +2 -2
  19. package/build/utils/crdt-utils.cjs +29 -0
  20. package/build/utils/crdt-utils.cjs.map +3 -3
  21. package/build/utils/crdt.cjs +16 -4
  22. package/build/utils/crdt.cjs.map +2 -2
  23. package/build-module/actions.mjs +17 -6
  24. package/build-module/actions.mjs.map +2 -2
  25. package/build-module/awareness/base-awareness.mjs +35 -0
  26. package/build-module/awareness/base-awareness.mjs.map +7 -0
  27. package/build-module/awareness/post-editor-awareness.mjs +4 -18
  28. package/build-module/awareness/post-editor-awareness.mjs.map +2 -2
  29. package/build-module/entities.mjs +15 -2
  30. package/build-module/entities.mjs.map +2 -2
  31. package/build-module/resolvers.mjs +40 -0
  32. package/build-module/resolvers.mjs.map +2 -2
  33. package/build-module/utils/block-selection-history.mjs +75 -0
  34. package/build-module/utils/block-selection-history.mjs.map +7 -0
  35. package/build-module/utils/crdt-selection.mjs +115 -0
  36. package/build-module/utils/crdt-selection.mjs.map +7 -0
  37. package/build-module/utils/crdt-user-selections.mjs +2 -2
  38. package/build-module/utils/crdt-user-selections.mjs.map +2 -2
  39. package/build-module/utils/crdt-utils.mjs +28 -0
  40. package/build-module/utils/crdt-utils.mjs.map +2 -2
  41. package/build-module/utils/crdt.mjs +18 -3
  42. package/build-module/utils/crdt.mjs.map +2 -2
  43. package/build-types/actions.d.ts.map +1 -1
  44. package/build-types/awareness/base-awareness.d.ts +19 -0
  45. package/build-types/awareness/base-awareness.d.ts.map +1 -0
  46. package/build-types/awareness/post-editor-awareness.d.ts +7 -8
  47. package/build-types/awareness/post-editor-awareness.d.ts.map +1 -1
  48. package/build-types/entities.d.ts.map +1 -1
  49. package/build-types/entity-types/test/attachment.test.d.ts +10 -0
  50. package/build-types/entity-types/test/attachment.test.d.ts.map +1 -0
  51. package/build-types/index.d.ts.map +1 -1
  52. package/build-types/resolvers.d.ts.map +1 -1
  53. package/build-types/types.d.ts +12 -2
  54. package/build-types/types.d.ts.map +1 -1
  55. package/build-types/utils/block-selection-history.d.ts +47 -0
  56. package/build-types/utils/block-selection-history.d.ts.map +1 -0
  57. package/build-types/utils/crdt-selection.d.ts +16 -0
  58. package/build-types/utils/crdt-selection.d.ts.map +1 -0
  59. package/build-types/utils/crdt-user-selections.d.ts.map +1 -1
  60. package/build-types/utils/crdt-utils.d.ts +12 -0
  61. package/build-types/utils/crdt-utils.d.ts.map +1 -1
  62. package/build-types/utils/crdt.d.ts +8 -14
  63. package/build-types/utils/crdt.d.ts.map +1 -1
  64. package/build-types/utils/test/block-selection-history.test.d.ts +2 -0
  65. package/build-types/utils/test/block-selection-history.test.d.ts.map +1 -0
  66. package/build-types/utils/test/crdt-blocks.d.ts +2 -0
  67. package/build-types/utils/test/crdt-blocks.d.ts.map +1 -0
  68. package/build-types/utils/test/crdt.d.ts +2 -0
  69. package/build-types/utils/test/crdt.d.ts.map +1 -0
  70. package/package.json +21 -19
  71. package/src/actions.js +40 -7
  72. package/src/awareness/base-awareness.ts +50 -0
  73. package/src/awareness/post-editor-awareness.ts +4 -24
  74. package/src/entities.js +18 -2
  75. package/src/entity-types/test/attachment.test.ts +4 -4
  76. package/src/resolvers.js +51 -0
  77. package/src/test/actions.js +402 -0
  78. package/src/test/resolvers.js +4 -0
  79. package/src/types.ts +12 -3
  80. package/src/utils/block-selection-history.ts +176 -0
  81. package/src/utils/crdt-selection.ts +205 -0
  82. package/src/utils/crdt-user-selections.ts +7 -3
  83. package/src/utils/crdt-utils.ts +54 -0
  84. package/src/utils/crdt.ts +36 -3
  85. package/src/utils/test/block-selection-history.test.ts +764 -0
@@ -0,0 +1,205 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { dispatch, select } from '@wordpress/data';
5
+ // @ts-expect-error No exported types.
6
+ import { store as blockEditorStore } from '@wordpress/block-editor';
7
+ // @ts-expect-error No exported types.
8
+ import { isUnmodifiedBlock } from '@wordpress/blocks';
9
+ import { type CRDTDoc, Y } from '@wordpress/sync';
10
+
11
+ /**
12
+ * Internal dependencies
13
+ */
14
+ import {
15
+ createBlockSelectionHistory,
16
+ YSelectionType,
17
+ type BlockSelectionHistory,
18
+ type YFullSelection,
19
+ type YSelection,
20
+ } from './block-selection-history';
21
+ import { findBlockByClientIdInDoc } from './crdt-utils';
22
+ import type { WPBlockSelection, WPSelection } from '../types';
23
+
24
+ // WeakMap to store BlockSelectionHistory instances per Y.Doc
25
+ const selectionHistoryMap = new WeakMap< CRDTDoc, BlockSelectionHistory >();
26
+
27
+ /**
28
+ * Get or create a BlockSelectionHistory instance for a given Y.Doc.
29
+ *
30
+ * @param ydoc The Y.Doc to get the selection history for
31
+ * @return The BlockSelectionHistory instance
32
+ */
33
+ function getBlockSelectionHistory( ydoc: CRDTDoc ): BlockSelectionHistory {
34
+ let history = selectionHistoryMap.get( ydoc );
35
+
36
+ if ( ! history ) {
37
+ history = createBlockSelectionHistory( ydoc );
38
+ selectionHistoryMap.set( ydoc, history );
39
+ }
40
+
41
+ return history;
42
+ }
43
+
44
+ export function getSelectionHistory( ydoc: CRDTDoc ): YFullSelection[] {
45
+ return getBlockSelectionHistory( ydoc ).getSelectionHistory();
46
+ }
47
+
48
+ export function updateSelectionHistory(
49
+ ydoc: CRDTDoc,
50
+ wpSelection: WPSelection
51
+ ): void {
52
+ return getBlockSelectionHistory( ydoc ).updateSelection( wpSelection );
53
+ }
54
+
55
+ /**
56
+ * Convert a YSelection to a WPBlockSelection.
57
+ * @param ySelection The YSelection (relative) to convert
58
+ * @param ydoc The Y.Doc to convert the selection to a block selection for
59
+ * @return The converted WPBlockSelection, or null if the conversion fails
60
+ */
61
+ function convertYSelectionToBlockSelection(
62
+ ySelection: YSelection,
63
+ ydoc: Y.Doc
64
+ ): WPBlockSelection | null {
65
+ if ( ySelection.type === YSelectionType.RelativeSelection ) {
66
+ const { relativePosition, attributeKey, clientId } = ySelection;
67
+
68
+ const absolutePosition = Y.createAbsolutePositionFromRelativePosition(
69
+ relativePosition,
70
+ ydoc
71
+ );
72
+
73
+ if ( absolutePosition ) {
74
+ return {
75
+ clientId,
76
+ attributeKey,
77
+ offset: absolutePosition.index,
78
+ };
79
+ }
80
+ } else if ( ySelection.type === YSelectionType.BlockSelection ) {
81
+ return {
82
+ clientId: ySelection.clientId,
83
+ attributeKey: undefined,
84
+ offset: undefined,
85
+ };
86
+ }
87
+
88
+ return null;
89
+ }
90
+
91
+ /**
92
+ * Given a Y.Doc and a selection history, find the most recent selection
93
+ * that exists in the document. Skip any selections that are not in the document.
94
+ * @param ydoc The Y.Doc to find the selection in
95
+ * @param selectionHistory The selection history to check
96
+ * @return The most recent selection that exists in the document, or null if no selection exists.
97
+ */
98
+ function findSelectionFromHistory(
99
+ ydoc: Y.Doc,
100
+ selectionHistory: YFullSelection[]
101
+ ): WPSelection | null {
102
+ // Try each position until we find one that exists in the document
103
+ 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,
115
+ ydoc
116
+ );
117
+ const endBlockSelection = convertYSelectionToBlockSelection(
118
+ end,
119
+ ydoc
120
+ );
121
+
122
+ if ( startBlockSelection === null || endBlockSelection === null ) {
123
+ continue;
124
+ }
125
+
126
+ return {
127
+ selectionStart: startBlockSelection,
128
+ selectionEnd: endBlockSelection,
129
+ };
130
+ }
131
+
132
+ return null;
133
+ }
134
+
135
+ /**
136
+ * Restore the selection to the most recent selection in history that is
137
+ * available in the document.
138
+ * @param selectionHistory The selection history to restore
139
+ * @param ydoc The Y.Doc where blocks are stored
140
+ */
141
+ export function restoreSelection(
142
+ selectionHistory: YFullSelection[],
143
+ ydoc: Y.Doc
144
+ ): void {
145
+ // Find the most recent selection in history that is available in
146
+ // the document.
147
+ const selectionToRestore = findSelectionFromHistory(
148
+ ydoc,
149
+ selectionHistory
150
+ );
151
+
152
+ if ( selectionToRestore === null ) {
153
+ // Case 1: No blocks in history are available for restoration.
154
+ // Do nothing.
155
+ return;
156
+ }
157
+
158
+ const { getBlock } = select( blockEditorStore );
159
+ const { resetSelection } = dispatch( blockEditorStore );
160
+ const { selectionStart, selectionEnd } = selectionToRestore;
161
+ const isSelectionInSameBlock =
162
+ selectionStart.clientId === selectionEnd.clientId;
163
+
164
+ if ( isSelectionInSameBlock ) {
165
+ // Case 2: After content is restored, the selection is available
166
+ // within the same block
167
+
168
+ const block = getBlock( selectionStart.clientId );
169
+ const isBlockEmpty = block && isUnmodifiedBlock( block );
170
+ const isBeginningOfEmptyBlock =
171
+ 0 === selectionStart.offset &&
172
+ 0 === selectionEnd.offset &&
173
+ isBlockEmpty;
174
+
175
+ if ( isBeginningOfEmptyBlock ) {
176
+ // Case 2a: When the content in a block has been removed after an
177
+ // undo, WordPress will set the selection to the block's client ID
178
+ // with an undefined startOffset and endOffset.
179
+ //
180
+ // To match the default behavior and tests, exclude the selection
181
+ // offset when resetting to position 0.
182
+ const selectionStartWithoutOffset = {
183
+ clientId: selectionStart.clientId,
184
+ };
185
+ const selectionEndWithoutOffset = {
186
+ clientId: selectionEnd.clientId,
187
+ };
188
+
189
+ resetSelection(
190
+ selectionStartWithoutOffset,
191
+ selectionEndWithoutOffset,
192
+ 0
193
+ );
194
+ } else {
195
+ // Case 2b: Otherwise, reset including the saved selection offset.
196
+ resetSelection( selectionStart, selectionEnd, 0 );
197
+ }
198
+ } else {
199
+ // Case 3: A multi-block selection was made. resetSelection() can only
200
+ // restore selections within the same block.
201
+ // When a multi-block selection is made, selectionEnd represents
202
+ // where the user's cursor ended.
203
+ resetSelection( selectionEnd, selectionEnd, 0 );
204
+ }
205
+ }
@@ -189,12 +189,16 @@ function getCursorPosition(
189
189
  blocks: YBlocks
190
190
  ): CursorPosition | null {
191
191
  const block = findBlockByClientId( selection.clientId, blocks );
192
- if ( ! block ) {
192
+ if (
193
+ ! block ||
194
+ ! selection.attributeKey ||
195
+ undefined === selection.offset
196
+ ) {
193
197
  return null;
194
198
  }
195
199
 
196
- const attributes = block.get( 'attributes' ) as Y.Map< Y.Text >;
197
- const currentYText = attributes.get( selection.attributeKey ) as Y.Text;
200
+ const attributes = block.get( 'attributes' );
201
+ const currentYText = attributes?.get( selection.attributeKey ) as Y.Text;
198
202
 
199
203
  const relativePosition = Y.createRelativePositionFromTypeIndex(
200
204
  currentYText,
@@ -3,6 +3,13 @@
3
3
  */
4
4
  import { Y } from '@wordpress/sync';
5
5
 
6
+ /**
7
+ * Internal dependencies
8
+ */
9
+ import type { YBlock, YBlocks } from './crdt-blocks';
10
+ import type { YPostRecord } from './crdt';
11
+ import { CRDT_RECORD_MAP_KEY } from '../sync';
12
+
6
13
  /**
7
14
  * A YMapRecord represents the shape of the data stored in a Y.Map.
8
15
  */
@@ -75,3 +82,50 @@ export function isYMap< T extends YMapRecord >(
75
82
  ): value is YMapWrap< T > {
76
83
  return value instanceof Y.Map;
77
84
  }
85
+
86
+ /**
87
+ * Given a block ID and a Y.Doc, find the block in the document.
88
+ *
89
+ * @param blockId The block ID to find
90
+ * @param ydoc The Y.Doc to find the block in
91
+ * @return The block, or null if the block is not found
92
+ */
93
+ export function findBlockByClientIdInDoc(
94
+ blockId: string,
95
+ ydoc: Y.Doc
96
+ ): YBlock | null {
97
+ const ymap = getRootMap< YPostRecord >( ydoc, CRDT_RECORD_MAP_KEY );
98
+ const blocks = ymap.get( 'blocks' );
99
+
100
+ if ( ! ( blocks instanceof Y.Array ) ) {
101
+ return null;
102
+ }
103
+
104
+ return findBlockByClientIdInBlocks( blockId, blocks );
105
+ }
106
+
107
+ function findBlockByClientIdInBlocks(
108
+ blockId: string,
109
+ blocks: YBlocks
110
+ ): YBlock | null {
111
+ for ( const block of blocks ) {
112
+ if ( block.get( 'clientId' ) === blockId ) {
113
+ return block;
114
+ }
115
+
116
+ const innerBlocks = block.get( 'innerBlocks' );
117
+
118
+ if ( innerBlocks && innerBlocks.length > 0 ) {
119
+ const innerBlock = findBlockByClientIdInBlocks(
120
+ blockId,
121
+ innerBlocks
122
+ );
123
+
124
+ if ( innerBlock ) {
125
+ return innerBlock;
126
+ }
127
+ }
128
+ }
129
+
130
+ return null;
131
+ }
package/src/utils/crdt.ts CHANGED
@@ -8,11 +8,18 @@ import fastDeepEqual from 'fast-deep-equal/es6/index.js';
8
8
  */
9
9
  // @ts-expect-error No exported types.
10
10
  import { __unstableSerializeAndClean } from '@wordpress/blocks';
11
- import { type CRDTDoc, type ObjectData, Y } from '@wordpress/sync';
11
+ import {
12
+ type CRDTDoc,
13
+ type ObjectData,
14
+ type SyncConfig,
15
+ Y,
16
+ } from '@wordpress/sync';
12
17
 
13
18
  /**
14
19
  * Internal dependencies
15
20
  */
21
+ import { BaseAwareness } from '../awareness/base-awareness';
22
+ import { type BaseState } from '../awareness/types';
16
23
  import {
17
24
  mergeCrdtBlocks,
18
25
  type Block,
@@ -27,6 +34,7 @@ import {
27
34
  WORDPRESS_META_KEY_FOR_CRDT_DOC_PERSISTENCE,
28
35
  } from '../sync';
29
36
  import type { WPSelection } from '../types';
37
+ import { updateSelectionHistory } from './crdt-selection';
30
38
  import {
31
39
  createYMap,
32
40
  getRootMap,
@@ -47,6 +55,7 @@ export type PostChanges = Partial< Post > & {
47
55
  export interface YPostRecord extends YMapRecord {
48
56
  author: number;
49
57
  blocks: YBlocks;
58
+ categories: number[];
50
59
  comment_status: string;
51
60
  date: string | null;
52
61
  excerpt: string;
@@ -66,6 +75,7 @@ export interface YPostRecord extends YMapRecord {
66
75
  const allowedPostProperties = new Set< string >( [
67
76
  'author',
68
77
  'blocks',
78
+ 'categories',
69
79
  'comment_status',
70
80
  'date',
71
81
  'excerpt',
@@ -94,7 +104,7 @@ const disallowedPostMetaKeys = new Set< string >( [
94
104
  * @param {Partial< ObjectData >} changes
95
105
  * @return {void}
96
106
  */
97
- export function defaultApplyChangesToCRDTDoc(
107
+ function defaultApplyChangesToCRDTDoc(
98
108
  ydoc: CRDTDoc,
99
109
  changes: ObjectData
100
110
  ): void {
@@ -240,9 +250,22 @@ export function applyPostChangesToCRDTDoc(
240
250
  }
241
251
  }
242
252
  } );
253
+
254
+ // Process changes that we don't want to persist to the CRDT document.
255
+ if ( changes.selection ) {
256
+ const selection = changes.selection;
257
+ // Persist selection changes at the end of the current event loop.
258
+ // This allows undo meta to be saved with the current selection before
259
+ // it is overwritten by the new selection from Gutenberg.
260
+ // Without this, selection history will already contain the latest
261
+ // selection (after this change) when the undo stack is saved.
262
+ setTimeout( () => {
263
+ updateSelectionHistory( ydoc, selection );
264
+ }, 0 );
265
+ }
243
266
  }
244
267
 
245
- export function defaultGetChangesFromCRDTDoc( crdtDoc: CRDTDoc ): ObjectData {
268
+ function defaultGetChangesFromCRDTDoc( crdtDoc: CRDTDoc ): ObjectData {
246
269
  return getRootMap( crdtDoc, CRDT_RECORD_MAP_KEY ).toJSON();
247
270
  }
248
271
 
@@ -382,6 +405,16 @@ export function getPostChangesFromCRDTDoc(
382
405
  return changes;
383
406
  }
384
407
 
408
+ /**
409
+ * This default sync config can be used for entities that are flat maps of
410
+ * primitive values and do not require custom logic to merge changes.
411
+ */
412
+ export const defaultSyncConfig: SyncConfig< BaseState > = {
413
+ applyChangesToCRDTDoc: defaultApplyChangesToCRDTDoc,
414
+ createAwareness: ( ydoc: CRDTDoc ) => new BaseAwareness( ydoc ),
415
+ getChangesFromCRDTDoc: defaultGetChangesFromCRDTDoc,
416
+ };
417
+
385
418
  /**
386
419
  * Extract the raw string value from a property that may be a string or an object
387
420
  * with a `raw` property (`RenderedText`).