@wordpress/core-data 7.40.2-next.v.202602271551.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.
- package/README.md +0 -27
- package/build/actions.cjs +9 -26
- package/build/actions.cjs.map +2 -2
- package/build/awareness/post-editor-awareness.cjs +45 -6
- package/build/awareness/post-editor-awareness.cjs.map +2 -2
- package/build/awareness/types.cjs.map +1 -1
- package/build/entities.cjs +36 -16
- package/build/entities.cjs.map +2 -2
- package/build/entity-provider.cjs +15 -6
- package/build/entity-provider.cjs.map +2 -2
- package/build/hooks/use-entity-prop.cjs +33 -2
- package/build/hooks/use-entity-prop.cjs.map +2 -2
- package/build/hooks/use-post-editor-awareness-state.cjs +120 -1
- package/build/hooks/use-post-editor-awareness-state.cjs.map +2 -2
- package/build/index.cjs +0 -3
- package/build/index.cjs.map +2 -2
- package/build/private-actions.cjs +27 -3
- package/build/private-actions.cjs.map +2 -2
- package/build/private-apis.cjs +14 -5
- package/build/private-apis.cjs.map +3 -3
- package/build/private-selectors.cjs +22 -2
- package/build/private-selectors.cjs.map +2 -2
- package/build/queried-data/actions.cjs +1 -1
- package/build/queried-data/actions.cjs.map +2 -2
- package/build/queried-data/reducer.cjs +19 -13
- package/build/queried-data/reducer.cjs.map +2 -2
- package/build/reducer.cjs +19 -2
- package/build/reducer.cjs.map +2 -2
- package/build/resolvers.cjs +24 -17
- package/build/resolvers.cjs.map +2 -2
- package/build/selectors.cjs +0 -15
- package/build/selectors.cjs.map +2 -2
- package/build/sync.cjs +12 -3
- package/build/sync.cjs.map +2 -2
- package/build/types.cjs.map +1 -1
- package/build/utils/block-selection-history.cjs +1 -1
- package/build/utils/block-selection-history.cjs.map +2 -2
- package/build/utils/crdt-blocks.cjs +85 -28
- package/build/utils/crdt-blocks.cjs.map +2 -2
- package/build/utils/crdt-selection.cjs +4 -1
- package/build/utils/crdt-selection.cjs.map +2 -2
- package/build/utils/crdt-text.cjs +52 -0
- package/build/utils/crdt-text.cjs.map +7 -0
- package/build/utils/crdt-user-selections.cjs +16 -6
- package/build/utils/crdt-user-selections.cjs.map +3 -3
- package/build/utils/crdt-utils.cjs +54 -2
- package/build/utils/crdt-utils.cjs.map +2 -2
- package/build/utils/crdt.cjs +26 -28
- package/build/utils/crdt.cjs.map +2 -2
- package/build-module/actions.mjs +14 -26
- package/build-module/actions.mjs.map +2 -2
- package/build-module/awareness/post-editor-awareness.mjs +47 -7
- package/build-module/awareness/post-editor-awareness.mjs.map +2 -2
- package/build-module/entities.mjs +37 -16
- package/build-module/entities.mjs.map +2 -2
- package/build-module/entity-provider.mjs +15 -6
- package/build-module/entity-provider.mjs.map +2 -2
- package/build-module/hooks/use-entity-prop.mjs +34 -3
- package/build-module/hooks/use-entity-prop.mjs.map +2 -2
- package/build-module/hooks/use-post-editor-awareness-state.mjs +117 -1
- package/build-module/hooks/use-post-editor-awareness-state.mjs.map +2 -2
- package/build-module/index.mjs +0 -2
- package/build-module/index.mjs.map +2 -2
- package/build-module/private-actions.mjs +24 -2
- package/build-module/private-actions.mjs.map +2 -2
- package/build-module/private-apis.mjs +21 -6
- package/build-module/private-apis.mjs.map +3 -3
- package/build-module/private-selectors.mjs +19 -1
- package/build-module/private-selectors.mjs.map +2 -2
- package/build-module/queried-data/actions.mjs +1 -1
- package/build-module/queried-data/actions.mjs.map +2 -2
- package/build-module/queried-data/reducer.mjs +19 -13
- package/build-module/queried-data/reducer.mjs.map +2 -2
- package/build-module/reducer.mjs +18 -2
- package/build-module/reducer.mjs.map +2 -2
- package/build-module/resolvers.mjs +24 -17
- package/build-module/resolvers.mjs.map +2 -2
- package/build-module/selectors.mjs +0 -14
- package/build-module/selectors.mjs.map +2 -2
- package/build-module/sync.mjs +8 -2
- package/build-module/sync.mjs.map +2 -2
- package/build-module/utils/block-selection-history.mjs +5 -2
- package/build-module/utils/block-selection-history.mjs.map +2 -2
- package/build-module/utils/crdt-blocks.mjs +84 -28
- package/build-module/utils/crdt-blocks.mjs.map +2 -2
- package/build-module/utils/crdt-selection.mjs +8 -2
- package/build-module/utils/crdt-selection.mjs.map +2 -2
- package/build-module/utils/crdt-text.mjs +26 -0
- package/build-module/utils/crdt-text.mjs.map +7 -0
- package/build-module/utils/crdt-user-selections.mjs +16 -7
- package/build-module/utils/crdt-user-selections.mjs.map +3 -3
- package/build-module/utils/crdt-utils.mjs +51 -1
- package/build-module/utils/crdt-utils.mjs.map +2 -2
- package/build-module/utils/crdt.mjs +26 -28
- package/build-module/utils/crdt.mjs.map +2 -2
- package/build-types/actions.d.ts +0 -11
- package/build-types/actions.d.ts.map +1 -1
- package/build-types/awareness/post-editor-awareness.d.ts +2 -2
- package/build-types/awareness/post-editor-awareness.d.ts.map +1 -1
- package/build-types/awareness/types.d.ts +6 -1
- package/build-types/awareness/types.d.ts.map +1 -1
- package/build-types/entities.d.ts +20 -1
- package/build-types/entities.d.ts.map +1 -1
- package/build-types/entity-provider.d.ts +11 -6
- package/build-types/entity-provider.d.ts.map +1 -1
- package/build-types/hooks/use-entity-prop.d.ts.map +1 -1
- package/build-types/hooks/use-post-editor-awareness-state.d.ts +40 -7
- package/build-types/hooks/use-post-editor-awareness-state.d.ts.map +1 -1
- package/build-types/index.d.ts +0 -3
- package/build-types/index.d.ts.map +1 -1
- package/build-types/parsed-blocks-cache.d.ts +10 -0
- package/build-types/parsed-blocks-cache.d.ts.map +1 -0
- package/build-types/private-actions.d.ts +12 -0
- package/build-types/private-actions.d.ts.map +1 -1
- package/build-types/private-apis.d.ts +21 -1
- package/build-types/private-apis.d.ts.map +1 -1
- package/build-types/private-selectors.d.ts +17 -0
- package/build-types/private-selectors.d.ts.map +1 -1
- package/build-types/queried-data/reducer.d.ts.map +1 -1
- package/build-types/reducer.d.ts +15 -0
- package/build-types/reducer.d.ts.map +1 -1
- package/build-types/resolvers.d.ts.map +1 -1
- package/build-types/selectors.d.ts +1 -9
- package/build-types/selectors.d.ts.map +1 -1
- package/build-types/sync.d.ts +2 -2
- package/build-types/sync.d.ts.map +1 -1
- package/build-types/types.d.ts +10 -2
- package/build-types/types.d.ts.map +1 -1
- package/build-types/utils/block-selection-history.d.ts.map +1 -1
- package/build-types/utils/crdt-blocks.d.ts +11 -0
- package/build-types/utils/crdt-blocks.d.ts.map +1 -1
- package/build-types/utils/crdt-selection.d.ts.map +1 -1
- package/build-types/utils/crdt-text.d.ts +16 -0
- package/build-types/utils/crdt-text.d.ts.map +1 -0
- package/build-types/utils/crdt-user-selections.d.ts +17 -4
- package/build-types/utils/crdt-user-selections.d.ts.map +1 -1
- package/build-types/utils/crdt-utils.d.ts +20 -0
- package/build-types/utils/crdt-utils.d.ts.map +1 -1
- package/build-types/utils/crdt.d.ts +13 -7
- package/build-types/utils/crdt.d.ts.map +1 -1
- package/build-types/utils/normalize-query-for-resolution.d.ts +12 -0
- package/build-types/utils/normalize-query-for-resolution.d.ts.map +1 -0
- package/build-types/utils/save-crdt-doc.d.ts +8 -0
- package/build-types/utils/save-crdt-doc.d.ts.map +1 -0
- package/build-types/utils/test/crdt-utils.d.ts +2 -0
- package/build-types/utils/test/crdt-utils.d.ts.map +1 -0
- package/package.json +18 -18
- package/src/actions.js +26 -41
- package/src/awareness/post-editor-awareness.ts +106 -7
- package/src/awareness/test/post-editor-awareness.ts +50 -10
- package/src/awareness/types.ts +7 -1
- package/src/entities.js +43 -17
- package/src/entity-provider.js +24 -11
- package/src/hooks/test/use-post-editor-awareness-state.ts +446 -3
- package/src/hooks/use-entity-prop.js +43 -3
- package/src/hooks/use-post-editor-awareness-state.ts +229 -7
- package/src/index.js +0 -6
- package/src/private-actions.js +43 -1
- package/src/{private-apis.js → private-apis.ts} +21 -3
- package/src/private-selectors.ts +43 -0
- package/src/queried-data/actions.js +1 -1
- package/src/queried-data/reducer.js +26 -14
- package/src/reducer.js +36 -1
- package/src/resolvers.js +33 -23
- package/src/selectors.ts +1 -32
- package/src/sync.ts +6 -0
- package/src/test/entities.js +204 -16
- package/src/test/private-actions.js +1 -1
- package/src/test/resolvers.js +144 -57
- package/src/test/store.js +116 -0
- package/src/types.ts +20 -2
- package/src/utils/block-selection-history.ts +5 -2
- package/src/utils/crdt-blocks.ts +159 -56
- package/src/utils/crdt-selection.ts +8 -2
- package/src/utils/crdt-text.ts +43 -0
- package/src/utils/crdt-user-selections.ts +29 -8
- package/src/utils/crdt-utils.ts +99 -0
- package/src/utils/crdt.ts +50 -40
- package/src/utils/test/crdt-blocks.ts +588 -24
- package/src/utils/test/crdt-user-selections.ts +5 -0
- package/src/utils/test/crdt-utils.ts +387 -0
- package/src/utils/test/crdt.ts +291 -54
package/src/test/store.js
CHANGED
|
@@ -113,6 +113,122 @@ describe( 'getEntityRecord', () => {
|
|
|
113
113
|
} );
|
|
114
114
|
} );
|
|
115
115
|
|
|
116
|
+
describe( 'getEntityRecords', () => {
|
|
117
|
+
const POSTS = [
|
|
118
|
+
createTestPost( 1 ),
|
|
119
|
+
createTestPost( 2 ),
|
|
120
|
+
createTestPost( 3 ),
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
let registry;
|
|
124
|
+
|
|
125
|
+
beforeEach( () => {
|
|
126
|
+
registry = createTestRegistry();
|
|
127
|
+
triggerFetch.mockReset();
|
|
128
|
+
} );
|
|
129
|
+
|
|
130
|
+
it( 'preserves collection when getEntityRecord resolves after getEntityRecords', async () => {
|
|
131
|
+
let resolveSlowFetch;
|
|
132
|
+
const slowFetchPromise = new Promise( ( resolve ) => {
|
|
133
|
+
resolveSlowFetch = resolve;
|
|
134
|
+
} );
|
|
135
|
+
|
|
136
|
+
triggerFetch.mockImplementation( ( { path } ) => {
|
|
137
|
+
// Single post fetch (e.g. /wp/v2/posts/1): return slow promise.
|
|
138
|
+
if ( /\/wp\/v2\/posts\/\d+/.test( path ) ) {
|
|
139
|
+
return slowFetchPromise;
|
|
140
|
+
}
|
|
141
|
+
// Collection fetch: return immediately.
|
|
142
|
+
return Promise.resolve( {
|
|
143
|
+
json: () => Promise.resolve( POSTS ),
|
|
144
|
+
headers: {
|
|
145
|
+
get: ( header ) => {
|
|
146
|
+
if ( header === 'X-WP-Total' ) {
|
|
147
|
+
return String( POSTS.length );
|
|
148
|
+
}
|
|
149
|
+
if ( header === 'X-WP-TotalPages' ) {
|
|
150
|
+
return '1';
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
} );
|
|
156
|
+
} );
|
|
157
|
+
|
|
158
|
+
const resolveSelectStore = registry.resolveSelect( coreDataStore );
|
|
159
|
+
|
|
160
|
+
// Start getEntityRecord first (slow), then getEntityRecords (fast).
|
|
161
|
+
const singlePromise = resolveSelectStore.getEntityRecord(
|
|
162
|
+
'postType',
|
|
163
|
+
'post',
|
|
164
|
+
1,
|
|
165
|
+
{ context: 'edit' }
|
|
166
|
+
);
|
|
167
|
+
await resolveSelectStore.getEntityRecords( 'postType', 'post', {
|
|
168
|
+
context: 'edit',
|
|
169
|
+
} );
|
|
170
|
+
|
|
171
|
+
// Now resolve the slow single-record fetch.
|
|
172
|
+
resolveSlowFetch( {
|
|
173
|
+
json: () => Promise.resolve( POSTS[ 0 ] ),
|
|
174
|
+
headers: { get: () => null },
|
|
175
|
+
} );
|
|
176
|
+
await singlePromise;
|
|
177
|
+
|
|
178
|
+
// Wait for all pending thunks to settle.
|
|
179
|
+
await new Promise( ( resolve ) => setTimeout( resolve, 0 ) );
|
|
180
|
+
|
|
181
|
+
const allPosts = registry
|
|
182
|
+
.select( coreDataStore )
|
|
183
|
+
.getEntityRecords( 'postType', 'post', { context: 'edit' } );
|
|
184
|
+
expect( allPosts.map( ( p ) => p.id ) ).toEqual( [ 1, 2, 3 ] );
|
|
185
|
+
} );
|
|
186
|
+
|
|
187
|
+
it( 'preserves collection when getEntityRecord is called after getEntityRecords', async () => {
|
|
188
|
+
triggerFetch.mockImplementation( ( { path } ) => {
|
|
189
|
+
// Collection fetch.
|
|
190
|
+
if ( ! /\/wp\/v2\/posts\/\d+/.test( path ) ) {
|
|
191
|
+
return Promise.resolve( {
|
|
192
|
+
json: () => Promise.resolve( POSTS ),
|
|
193
|
+
headers: {
|
|
194
|
+
get: ( header ) => {
|
|
195
|
+
if ( header === 'X-WP-Total' ) {
|
|
196
|
+
return String( POSTS.length );
|
|
197
|
+
}
|
|
198
|
+
if ( header === 'X-WP-TotalPages' ) {
|
|
199
|
+
return '1';
|
|
200
|
+
}
|
|
201
|
+
return null;
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
} );
|
|
205
|
+
}
|
|
206
|
+
// Single post fetch.
|
|
207
|
+
return Promise.resolve( {
|
|
208
|
+
json: () => Promise.resolve( POSTS[ 0 ] ),
|
|
209
|
+
headers: { get: () => null },
|
|
210
|
+
} );
|
|
211
|
+
} );
|
|
212
|
+
|
|
213
|
+
const resolveSelectStore = registry.resolveSelect( coreDataStore );
|
|
214
|
+
|
|
215
|
+
// First resolve the collection.
|
|
216
|
+
await resolveSelectStore.getEntityRecords( 'postType', 'post', {
|
|
217
|
+
context: 'edit',
|
|
218
|
+
} );
|
|
219
|
+
|
|
220
|
+
// Then resolve a single record.
|
|
221
|
+
await resolveSelectStore.getEntityRecord( 'postType', 'post', 1, {
|
|
222
|
+
context: 'edit',
|
|
223
|
+
} );
|
|
224
|
+
|
|
225
|
+
const allPosts = registry
|
|
226
|
+
.select( coreDataStore )
|
|
227
|
+
.getEntityRecords( 'postType', 'post', { context: 'edit' } );
|
|
228
|
+
expect( allPosts.map( ( p ) => p.id ) ).toEqual( [ 1, 2, 3 ] );
|
|
229
|
+
} );
|
|
230
|
+
} );
|
|
231
|
+
|
|
116
232
|
describe( 'clearEntityRecordEdits', () => {
|
|
117
233
|
let registry;
|
|
118
234
|
|
package/src/types.ts
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* WordPress dependencies
|
|
3
3
|
*/
|
|
4
|
-
import type { Y } from '@wordpress/sync';
|
|
4
|
+
import type { ConnectionStatusDisconnected, Y } from '@wordpress/sync';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Internal dependencies
|
|
8
8
|
*/
|
|
9
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
SelectionType,
|
|
11
|
+
SelectionDirection,
|
|
12
|
+
} from './utils/crdt-user-selections';
|
|
13
|
+
|
|
14
|
+
export type { ConnectionStatus } from '@wordpress/sync';
|
|
15
|
+
|
|
16
|
+
export type ConnectionError = NonNullable<
|
|
17
|
+
ConnectionStatusDisconnected[ 'error' ]
|
|
18
|
+
>;
|
|
10
19
|
|
|
11
20
|
export interface AnyFunction {
|
|
12
21
|
( ...args: any[] ): any;
|
|
@@ -86,6 +95,8 @@ export type SelectionInOneBlock = {
|
|
|
86
95
|
type: SelectionType.SelectionInOneBlock;
|
|
87
96
|
cursorStartPosition: CursorPosition;
|
|
88
97
|
cursorEndPosition: CursorPosition;
|
|
98
|
+
// The direction of the selection, indicating where the caret sits.
|
|
99
|
+
selectionDirection?: SelectionDirection;
|
|
89
100
|
};
|
|
90
101
|
|
|
91
102
|
export type SelectionInMultipleBlocks = {
|
|
@@ -95,6 +106,8 @@ export type SelectionInMultipleBlocks = {
|
|
|
95
106
|
type: SelectionType.SelectionInMultipleBlocks;
|
|
96
107
|
cursorStartPosition: CursorPosition;
|
|
97
108
|
cursorEndPosition: CursorPosition;
|
|
109
|
+
// The direction of the selection, indicating where the caret sits.
|
|
110
|
+
selectionDirection?: SelectionDirection;
|
|
98
111
|
};
|
|
99
112
|
|
|
100
113
|
export type SelectionWholeBlock = {
|
|
@@ -111,3 +124,8 @@ export type SelectionState =
|
|
|
111
124
|
| SelectionInOneBlock
|
|
112
125
|
| SelectionInMultipleBlocks
|
|
113
126
|
| SelectionWholeBlock;
|
|
127
|
+
|
|
128
|
+
export interface ResolvedSelection {
|
|
129
|
+
richTextOffset: number | null;
|
|
130
|
+
localClientId: string | null;
|
|
131
|
+
}
|
|
@@ -9,7 +9,10 @@ import { Y } from '@wordpress/sync';
|
|
|
9
9
|
/**
|
|
10
10
|
* Internal dependencies
|
|
11
11
|
*/
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
findBlockByClientIdInDoc,
|
|
14
|
+
richTextOffsetToHtmlIndex,
|
|
15
|
+
} from './crdt-utils';
|
|
13
16
|
import type { WPBlockSelection, WPSelection } from '../types';
|
|
14
17
|
|
|
15
18
|
// Default size for selection history (not including current selection)
|
|
@@ -163,7 +166,7 @@ function convertWPBlockSelectionToSelection(
|
|
|
163
166
|
const offset = selection.offset ?? 0;
|
|
164
167
|
const relativePosition = Y.createRelativePositionFromTypeIndex(
|
|
165
168
|
changedYText,
|
|
166
|
-
offset
|
|
169
|
+
richTextOffsetToHtmlIndex( changedYText.toString(), offset )
|
|
167
170
|
);
|
|
168
171
|
|
|
169
172
|
return {
|
package/src/utils/crdt-blocks.ts
CHANGED
|
@@ -16,15 +16,22 @@ import { Y } from '@wordpress/sync';
|
|
|
16
16
|
* Internal dependencies
|
|
17
17
|
*/
|
|
18
18
|
import { createYMap, type YMapRecord, type YMapWrap } from './crdt-utils';
|
|
19
|
+
import { getCachedRichTextData } from './crdt-text';
|
|
19
20
|
import { Delta } from '../sync';
|
|
20
21
|
|
|
21
22
|
interface BlockAttributes {
|
|
22
23
|
[ key: string ]: unknown;
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
interface BlockAttributeType {
|
|
27
|
+
role?: string;
|
|
28
|
+
type?: string;
|
|
29
|
+
query?: Record< string, BlockAttributeType >;
|
|
30
|
+
}
|
|
31
|
+
|
|
25
32
|
interface BlockType {
|
|
33
|
+
attributes?: Record< string, BlockAttributeType >;
|
|
26
34
|
name: string;
|
|
27
|
-
attributes?: Record< string, { type?: string } >;
|
|
28
35
|
}
|
|
29
36
|
|
|
30
37
|
// A block as represented in Gutenberg's data store.
|
|
@@ -57,14 +64,49 @@ export type YBlockAttributes = Y.Map< Y.Text | unknown >;
|
|
|
57
64
|
|
|
58
65
|
const serializableBlocksCache = new WeakMap< WeakKey, Block[] >();
|
|
59
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Recursively walk an attribute value and convert any RichTextData instances
|
|
69
|
+
* to their string (HTML) representation. This is necessary for array-type and
|
|
70
|
+
* object-type attributes, which can contain nested RichTextData.
|
|
71
|
+
*
|
|
72
|
+
* @param value The attribute value to serialize.
|
|
73
|
+
* @return The value with all RichTextData instances replaced by strings.
|
|
74
|
+
*/
|
|
75
|
+
function serializeAttributeValue( value: unknown ): unknown {
|
|
76
|
+
if ( value instanceof RichTextData ) {
|
|
77
|
+
return value.valueOf();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// e.g. core/table `body`: [ { cells: [ { content: RichTextData } ] } ]
|
|
81
|
+
if ( Array.isArray( value ) ) {
|
|
82
|
+
return value.map( serializeAttributeValue );
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// e.g. a single row inside core/table `body`: { cells: [ ... ] }
|
|
86
|
+
if ( value && typeof value === 'object' ) {
|
|
87
|
+
const result: Record< string, unknown > = {};
|
|
88
|
+
|
|
89
|
+
for ( const [ k, v ] of Object.entries( value ) ) {
|
|
90
|
+
result[ k ] = serializeAttributeValue( v );
|
|
91
|
+
}
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
|
|
60
98
|
function makeBlockAttributesSerializable(
|
|
99
|
+
blockName: string,
|
|
61
100
|
attributes: BlockAttributes
|
|
62
101
|
): BlockAttributes {
|
|
63
102
|
const newAttributes = { ...attributes };
|
|
64
103
|
for ( const [ key, value ] of Object.entries( attributes ) ) {
|
|
65
|
-
if (
|
|
66
|
-
newAttributes[ key ]
|
|
104
|
+
if ( isLocalAttribute( blockName, key ) ) {
|
|
105
|
+
delete newAttributes[ key ];
|
|
106
|
+
continue;
|
|
67
107
|
}
|
|
108
|
+
|
|
109
|
+
newAttributes[ key ] = serializeAttributeValue( value );
|
|
68
110
|
}
|
|
69
111
|
return newAttributes;
|
|
70
112
|
}
|
|
@@ -76,12 +118,91 @@ function makeBlocksSerializable( blocks: Block[] ): Block[] {
|
|
|
76
118
|
return {
|
|
77
119
|
...rest,
|
|
78
120
|
name,
|
|
79
|
-
attributes: makeBlockAttributesSerializable( attributes ),
|
|
121
|
+
attributes: makeBlockAttributesSerializable( name, attributes ),
|
|
80
122
|
innerBlocks: makeBlocksSerializable( innerBlocks ),
|
|
81
123
|
};
|
|
82
124
|
} );
|
|
83
125
|
}
|
|
84
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Recursively walk an attribute value and convert any strings that correspond
|
|
129
|
+
* to rich-text schema nodes into RichTextData instances. This is the inverse
|
|
130
|
+
* of serializeAttributeValue and handles nested structures like table cells.
|
|
131
|
+
*
|
|
132
|
+
* @param schema The attribute type definition for this value.
|
|
133
|
+
* @param value The attribute value from CRDT (toJSON).
|
|
134
|
+
* @return The value with rich-text strings replaced by RichTextData.
|
|
135
|
+
*/
|
|
136
|
+
function deserializeAttributeValue(
|
|
137
|
+
schema: BlockAttributeType | undefined,
|
|
138
|
+
value: unknown
|
|
139
|
+
): unknown {
|
|
140
|
+
if ( schema?.type === 'rich-text' && typeof value === 'string' ) {
|
|
141
|
+
return getCachedRichTextData( value );
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// e.g. core/table `body`: [ { cells: [ { content: RichTextData } ] } ]
|
|
145
|
+
if ( Array.isArray( value ) ) {
|
|
146
|
+
return value.map( ( item ) =>
|
|
147
|
+
deserializeAttributeValue( schema, item )
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// e.g. a single row inside core/table `body`: { cells: [ ... ] }
|
|
152
|
+
if ( value && typeof value === 'object' ) {
|
|
153
|
+
const result: Record< string, unknown > = {};
|
|
154
|
+
|
|
155
|
+
for ( const [ key, innerValue ] of Object.entries(
|
|
156
|
+
value as Record< string, unknown >
|
|
157
|
+
) ) {
|
|
158
|
+
result[ key ] = deserializeAttributeValue(
|
|
159
|
+
schema?.query?.[ key ],
|
|
160
|
+
innerValue
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return value;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Convert blocks from their CRDT-serialized form back to the runtime form
|
|
172
|
+
* expected by the block editor. Rich-text attributes are stored as Y.Text in
|
|
173
|
+
* the CRDT document, which serializes to plain strings via toJSON(). This
|
|
174
|
+
* function restores them to RichTextData instances so that block edit
|
|
175
|
+
* components that rely on RichTextData methods (e.g. `.text`) work correctly.
|
|
176
|
+
*
|
|
177
|
+
* @param blocks Blocks as extracted from the CRDT document via toJSON().
|
|
178
|
+
* @return Blocks with rich-text attributes restored to RichTextData.
|
|
179
|
+
*/
|
|
180
|
+
export function deserializeBlockAttributes( blocks: Block[] ): Block[] {
|
|
181
|
+
return blocks.map( ( block: Block ) => {
|
|
182
|
+
const { name, innerBlocks, attributes, ...rest } = block;
|
|
183
|
+
|
|
184
|
+
const newAttributes = { ...attributes };
|
|
185
|
+
|
|
186
|
+
for ( const [ key, value ] of Object.entries( attributes ) ) {
|
|
187
|
+
const schema = getBlockAttributeType( name, key );
|
|
188
|
+
|
|
189
|
+
if ( schema ) {
|
|
190
|
+
newAttributes[ key ] = deserializeAttributeValue(
|
|
191
|
+
schema,
|
|
192
|
+
value
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
...rest,
|
|
199
|
+
name,
|
|
200
|
+
attributes: newAttributes,
|
|
201
|
+
innerBlocks: deserializeBlockAttributes( innerBlocks ?? [] ),
|
|
202
|
+
};
|
|
203
|
+
} );
|
|
204
|
+
}
|
|
205
|
+
|
|
85
206
|
/**
|
|
86
207
|
* @param {any} gblock
|
|
87
208
|
* @param {Y.Map} yblock
|
|
@@ -202,12 +323,7 @@ export function mergeCrdtBlocks(
|
|
|
202
323
|
makeBlocksSerializable( incomingBlocks )
|
|
203
324
|
);
|
|
204
325
|
}
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
// Ensure we skip blocks that we don't want to sync at the moment
|
|
208
|
-
const blocksToSync = allBlocks.filter( ( block ) =>
|
|
209
|
-
shouldBlockBeSynced( block )
|
|
210
|
-
);
|
|
326
|
+
const blocksToSync = serializableBlocksCache.get( incomingBlocks ) ?? [];
|
|
211
327
|
|
|
212
328
|
// This is a rudimentary diff implementation similar to the y-prosemirror diffing
|
|
213
329
|
// approach.
|
|
@@ -382,32 +498,6 @@ export function mergeCrdtBlocks(
|
|
|
382
498
|
}
|
|
383
499
|
}
|
|
384
500
|
|
|
385
|
-
/**
|
|
386
|
-
* Determine if a block should be synced.
|
|
387
|
-
*
|
|
388
|
-
* Ex: A gallery block should not be synced until the images have been
|
|
389
|
-
* uploaded to WordPress, and their url is available. Before that,
|
|
390
|
-
* it's not possible to access the blobs on a client as those are
|
|
391
|
-
* local.
|
|
392
|
-
*
|
|
393
|
-
* @param block The block to check.
|
|
394
|
-
* @return True if the block should be synced, false otherwise.
|
|
395
|
-
*/
|
|
396
|
-
function shouldBlockBeSynced( block: Block ): boolean {
|
|
397
|
-
// Verify that the gallery block is ready to be synced.
|
|
398
|
-
// This means that, all images have had their blobs converted to full URLs.
|
|
399
|
-
// Checking for only the blobs ensures that blocks that have just been inserted work as well.
|
|
400
|
-
if ( 'core/gallery' === block.name ) {
|
|
401
|
-
return ! block.innerBlocks.some(
|
|
402
|
-
( innerBlock ) =>
|
|
403
|
-
innerBlock.attributes && innerBlock.attributes.blob
|
|
404
|
-
);
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
// Allow all other blocks to be synced.
|
|
408
|
-
return true;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
501
|
/**
|
|
412
502
|
* Update a single attribute on a Yjs block attributes map (currentAttributes).
|
|
413
503
|
*
|
|
@@ -448,38 +538,35 @@ function updateYBlockAttribute(
|
|
|
448
538
|
}
|
|
449
539
|
}
|
|
450
540
|
|
|
451
|
-
// Cached types
|
|
452
|
-
let cachedBlockAttributeTypes: Map< string, Map< string,
|
|
541
|
+
// Cached block attribute types, populated once from getBlockTypes().
|
|
542
|
+
let cachedBlockAttributeTypes: Map< string, Map< string, BlockAttributeType > >;
|
|
453
543
|
|
|
454
544
|
/**
|
|
455
|
-
* Get the
|
|
545
|
+
* Get the attribute type definition for a block attribute.
|
|
456
546
|
*
|
|
457
547
|
* @param blockName The name of the block, e.g. 'core/paragraph'.
|
|
458
548
|
* @param attributeName The name of the attribute, e.g. 'content'.
|
|
459
|
-
* @return The type of the attribute
|
|
549
|
+
* @return The type definition of the attribute.
|
|
460
550
|
*/
|
|
461
551
|
function getBlockAttributeType(
|
|
462
552
|
blockName: string,
|
|
463
553
|
attributeName: string
|
|
464
|
-
):
|
|
554
|
+
): BlockAttributeType | undefined {
|
|
465
555
|
if ( ! cachedBlockAttributeTypes ) {
|
|
466
556
|
// Parse the attributes for all blocks once.
|
|
467
|
-
cachedBlockAttributeTypes = new Map
|
|
557
|
+
cachedBlockAttributeTypes = new Map();
|
|
468
558
|
|
|
469
559
|
for ( const blockType of getBlockTypes() as BlockType[] ) {
|
|
470
|
-
const blockAttributeTypeMap = new Map< string, string >();
|
|
471
|
-
|
|
472
|
-
for ( const [ name, definition ] of Object.entries(
|
|
473
|
-
blockType.attributes ?? {}
|
|
474
|
-
) ) {
|
|
475
|
-
if ( definition.type ) {
|
|
476
|
-
blockAttributeTypeMap.set( name, definition.type );
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
560
|
cachedBlockAttributeTypes.set(
|
|
481
561
|
blockType.name,
|
|
482
|
-
|
|
562
|
+
new Map< string, BlockAttributeType >(
|
|
563
|
+
Object.entries( blockType.attributes ?? {} ).map(
|
|
564
|
+
( [ name, definition ] ) => {
|
|
565
|
+
const { role, type, query } = definition;
|
|
566
|
+
return [ name, { role, type, query } ];
|
|
567
|
+
}
|
|
568
|
+
)
|
|
569
|
+
)
|
|
483
570
|
);
|
|
484
571
|
}
|
|
485
572
|
}
|
|
@@ -503,11 +590,13 @@ function isExpectedAttributeType(
|
|
|
503
590
|
const expectedAttributeType = getBlockAttributeType(
|
|
504
591
|
blockName,
|
|
505
592
|
attributeName
|
|
506
|
-
);
|
|
593
|
+
)?.type;
|
|
507
594
|
|
|
508
595
|
if ( expectedAttributeType === 'rich-text' ) {
|
|
509
596
|
return attributeValue instanceof Y.Text;
|
|
510
|
-
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if ( expectedAttributeType === 'string' ) {
|
|
511
600
|
return typeof attributeValue === 'string';
|
|
512
601
|
}
|
|
513
602
|
|
|
@@ -515,6 +604,18 @@ function isExpectedAttributeType(
|
|
|
515
604
|
return true;
|
|
516
605
|
}
|
|
517
606
|
|
|
607
|
+
/**
|
|
608
|
+
* Given a block name and attribute key, return true if the attribute is local
|
|
609
|
+
* and should not be synced.
|
|
610
|
+
*
|
|
611
|
+
* @param blockName The name of the block, e.g. 'core/image'.
|
|
612
|
+
* @param attributeName The name of the attribute to check, e.g. 'blob'.
|
|
613
|
+
* @return True if the attribute is local, false otherwise.
|
|
614
|
+
*/
|
|
615
|
+
function isLocalAttribute( blockName: string, attributeName: string ): boolean {
|
|
616
|
+
return 'local' === getBlockAttributeType( blockName, attributeName )?.role;
|
|
617
|
+
}
|
|
618
|
+
|
|
518
619
|
/**
|
|
519
620
|
* Given a block name and attribute key, return true if the attribute is rich-text typed.
|
|
520
621
|
*
|
|
@@ -526,7 +627,9 @@ function isRichTextAttribute(
|
|
|
526
627
|
blockName: string,
|
|
527
628
|
attributeName: string
|
|
528
629
|
): boolean {
|
|
529
|
-
return
|
|
630
|
+
return (
|
|
631
|
+
'rich-text' === getBlockAttributeType( blockName, attributeName )?.type
|
|
632
|
+
);
|
|
530
633
|
}
|
|
531
634
|
|
|
532
635
|
let localDoc: Y.Doc;
|
|
@@ -18,7 +18,10 @@ import {
|
|
|
18
18
|
type YFullSelection,
|
|
19
19
|
type YSelection,
|
|
20
20
|
} from './block-selection-history';
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
findBlockByClientIdInDoc,
|
|
23
|
+
htmlIndexToRichTextOffset,
|
|
24
|
+
} from './crdt-utils';
|
|
22
25
|
import type { WPBlockSelection, WPSelection } from '../types';
|
|
23
26
|
|
|
24
27
|
// WeakMap to store BlockSelectionHistory instances per Y.Doc
|
|
@@ -74,7 +77,10 @@ function convertYSelectionToBlockSelection(
|
|
|
74
77
|
return {
|
|
75
78
|
clientId,
|
|
76
79
|
attributeKey,
|
|
77
|
-
offset:
|
|
80
|
+
offset: htmlIndexToRichTextOffset(
|
|
81
|
+
absolutePosition.type.toString(),
|
|
82
|
+
absolutePosition.index
|
|
83
|
+
),
|
|
78
84
|
};
|
|
79
85
|
}
|
|
80
86
|
} else if ( ySelection.type === YSelectionType.BlockSelection ) {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress dependencies
|
|
3
|
+
*/
|
|
4
|
+
import { RichTextData } from '@wordpress/rich-text';
|
|
5
|
+
|
|
6
|
+
const RICH_TEXT_CACHE_MAX_SIZE = 500;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Returns a function that converts HTML strings to RichTextData instances,
|
|
10
|
+
* using a FIFO cache bounded by `maxSize` to avoid re-parsing identical
|
|
11
|
+
* strings. Repeated calls with the same string return the cached instance
|
|
12
|
+
* without re-running the HTML parser and DOM traversal.
|
|
13
|
+
*
|
|
14
|
+
* @param maxSize Maximum number of entries to hold in the cache.
|
|
15
|
+
* @return A cached version of RichTextData.fromHTMLString.
|
|
16
|
+
*/
|
|
17
|
+
export function createRichTextDataCache(
|
|
18
|
+
maxSize: number
|
|
19
|
+
): ( value: string ) => RichTextData {
|
|
20
|
+
const cache = new Map< string, RichTextData >();
|
|
21
|
+
|
|
22
|
+
return function ( value: string ): RichTextData {
|
|
23
|
+
const cached = cache.get( value );
|
|
24
|
+
|
|
25
|
+
if ( cached ) {
|
|
26
|
+
return cached;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const result = RichTextData.fromHTMLString( value );
|
|
30
|
+
|
|
31
|
+
if ( cache.size >= maxSize ) {
|
|
32
|
+
// Evict the oldest entry (Map preserves insertion order).
|
|
33
|
+
cache.delete( cache.keys().next().value! );
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
cache.set( value, result );
|
|
37
|
+
return result;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const getCachedRichTextData = createRichTextDataCache(
|
|
42
|
+
RICH_TEXT_CACHE_MAX_SIZE
|
|
43
|
+
);
|
|
@@ -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
|
|
48
|
-
* @param selectionEnd
|
|
49
|
-
* @param yDoc
|
|
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(
|