@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.
- 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 +50 -23
- 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 +16 -6
- package/build/resolvers.cjs.map +2 -2
- package/build/selectors.cjs +0 -15
- package/build/selectors.cjs.map +2 -2
- package/build/sync.cjs +11 -5
- 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 +125 -49
- package/build/utils/crdt-blocks.cjs.map +2 -2
- package/build/utils/crdt-selection.cjs +50 -19
- 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 +38 -29
- 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 +53 -24
- 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 +16 -6
- 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 +7 -3
- 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 +124 -49
- package/build-module/utils/crdt-blocks.mjs.map +2 -2
- package/build-module/utils/crdt-selection.mjs +53 -20
- 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 +42 -34
- 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 +11 -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 +12 -1
- package/build-types/utils/crdt-blocks.d.ts.map +1 -1
- package/build-types/utils/crdt-selection.d.ts +10 -0
- 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 +14 -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 +62 -24
- 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 +23 -7
- package/src/selectors.ts +1 -32
- package/src/sync.ts +6 -2
- package/src/test/entities.js +231 -14
- package/src/test/private-actions.js +1 -1
- package/src/test/resolvers.js +64 -11
- package/src/test/store.js +116 -0
- package/src/types.ts +21 -2
- package/src/utils/block-selection-history.ts +5 -2
- package/src/utils/crdt-blocks.ts +250 -81
- package/src/utils/crdt-selection.ts +92 -26
- 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 +72 -46
- package/src/utils/test/crdt-blocks.ts +1177 -22
- 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 +423 -60
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.
|
|
@@ -39,7 +46,7 @@ export interface Block {
|
|
|
39
46
|
}
|
|
40
47
|
|
|
41
48
|
// A block as represented in the CRDT document (Y.Map).
|
|
42
|
-
interface YBlockRecord extends YMapRecord {
|
|
49
|
+
export interface YBlockRecord extends YMapRecord {
|
|
43
50
|
attributes: YBlockAttributes;
|
|
44
51
|
clientId: string;
|
|
45
52
|
innerBlocks: YBlocks;
|
|
@@ -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.
|
|
@@ -283,43 +399,29 @@ export function mergeCrdtBlocks(
|
|
|
283
399
|
|
|
284
400
|
Object.entries( value ).forEach(
|
|
285
401
|
( [ attributeName, attributeValue ] ) => {
|
|
286
|
-
if (
|
|
287
|
-
fastDeepEqual(
|
|
288
|
-
currentAttributes?.get( attributeName ),
|
|
289
|
-
attributeValue
|
|
290
|
-
)
|
|
291
|
-
) {
|
|
292
|
-
return;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
402
|
const currentAttribute =
|
|
296
|
-
currentAttributes
|
|
297
|
-
|
|
403
|
+
currentAttributes?.get( attributeName );
|
|
404
|
+
|
|
405
|
+
const isExpectedType = isExpectedAttributeType(
|
|
298
406
|
block.name,
|
|
299
|
-
attributeName
|
|
407
|
+
attributeName,
|
|
408
|
+
currentAttribute
|
|
300
409
|
);
|
|
301
410
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
currentAttributes.has( attributeName ) &&
|
|
306
|
-
currentAttribute instanceof Y.Text
|
|
307
|
-
) {
|
|
308
|
-
// Rich text values are stored as persistent Y.Text instances.
|
|
309
|
-
// Update the value with a delta in place.
|
|
310
|
-
mergeRichTextUpdate(
|
|
411
|
+
const isAttributeChanged =
|
|
412
|
+
! isExpectedType ||
|
|
413
|
+
! fastDeepEqual(
|
|
311
414
|
currentAttribute,
|
|
312
|
-
attributeValue
|
|
313
|
-
cursorPosition
|
|
415
|
+
attributeValue
|
|
314
416
|
);
|
|
315
|
-
|
|
316
|
-
|
|
417
|
+
|
|
418
|
+
if ( isAttributeChanged ) {
|
|
419
|
+
updateYBlockAttribute(
|
|
420
|
+
block.name,
|
|
317
421
|
attributeName,
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
attributeValue
|
|
322
|
-
)
|
|
422
|
+
attributeValue,
|
|
423
|
+
currentAttributes,
|
|
424
|
+
cursorPosition
|
|
323
425
|
);
|
|
324
426
|
}
|
|
325
427
|
}
|
|
@@ -397,69 +499,136 @@ export function mergeCrdtBlocks(
|
|
|
397
499
|
}
|
|
398
500
|
|
|
399
501
|
/**
|
|
400
|
-
*
|
|
502
|
+
* Update a single attribute on a Yjs block attributes map (currentAttributes).
|
|
401
503
|
*
|
|
402
|
-
*
|
|
403
|
-
*
|
|
404
|
-
*
|
|
405
|
-
* local.
|
|
504
|
+
* For rich-text attributes that already exist as Y.Text instances, the update
|
|
505
|
+
* is applied as a delta merge so that concurrent edits are preserved. All
|
|
506
|
+
* other attributes are replaced wholesale via `createNewYAttributeValue`.
|
|
406
507
|
*
|
|
407
|
-
* @param
|
|
408
|
-
* @
|
|
508
|
+
* @param blockName The block type name, e.g. 'core/paragraph'.
|
|
509
|
+
* @param attributeName The name of the attribute to update, e.g. 'content'.
|
|
510
|
+
* @param attributeValue The new value for the attribute.
|
|
511
|
+
* @param currentAttributes The Y.Map holding the block's current attributes.
|
|
512
|
+
* @param cursorPosition The local cursor position, used when merging rich-text deltas.
|
|
409
513
|
*/
|
|
410
|
-
function
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
514
|
+
function updateYBlockAttribute(
|
|
515
|
+
blockName: string,
|
|
516
|
+
attributeName: string,
|
|
517
|
+
attributeValue: unknown,
|
|
518
|
+
currentAttributes: YBlockAttributes,
|
|
519
|
+
cursorPosition: number | null
|
|
520
|
+
): void {
|
|
521
|
+
const isRichText = isRichTextAttribute( blockName, attributeName );
|
|
522
|
+
const currentAttribute = currentAttributes.get( attributeName );
|
|
523
|
+
|
|
524
|
+
if (
|
|
525
|
+
isRichText &&
|
|
526
|
+
'string' === typeof attributeValue &&
|
|
527
|
+
currentAttributes.has( attributeName ) &&
|
|
528
|
+
currentAttribute instanceof Y.Text
|
|
529
|
+
) {
|
|
530
|
+
// Rich text values are stored as persistent Y.Text instances.
|
|
531
|
+
// Update the value with a delta in place.
|
|
532
|
+
mergeRichTextUpdate( currentAttribute, attributeValue, cursorPosition );
|
|
533
|
+
} else {
|
|
534
|
+
currentAttributes.set(
|
|
535
|
+
attributeName,
|
|
536
|
+
createNewYAttributeValue( blockName, attributeName, attributeValue )
|
|
418
537
|
);
|
|
419
538
|
}
|
|
420
|
-
|
|
421
|
-
// Allow all other blocks to be synced.
|
|
422
|
-
return true;
|
|
423
539
|
}
|
|
424
540
|
|
|
425
|
-
//
|
|
426
|
-
let
|
|
541
|
+
// Cached block attribute types, populated once from getBlockTypes().
|
|
542
|
+
let cachedBlockAttributeTypes: Map< string, Map< string, BlockAttributeType > >;
|
|
427
543
|
|
|
428
544
|
/**
|
|
429
|
-
*
|
|
545
|
+
* Get the attribute type definition for a block attribute.
|
|
430
546
|
*
|
|
431
547
|
* @param blockName The name of the block, e.g. 'core/paragraph'.
|
|
432
|
-
* @param attributeName The name of the attribute
|
|
433
|
-
* @return
|
|
548
|
+
* @param attributeName The name of the attribute, e.g. 'content'.
|
|
549
|
+
* @return The type definition of the attribute.
|
|
434
550
|
*/
|
|
435
|
-
function
|
|
551
|
+
function getBlockAttributeType(
|
|
436
552
|
blockName: string,
|
|
437
553
|
attributeName: string
|
|
438
|
-
):
|
|
439
|
-
if ( !
|
|
554
|
+
): BlockAttributeType | undefined {
|
|
555
|
+
if ( ! cachedBlockAttributeTypes ) {
|
|
440
556
|
// Parse the attributes for all blocks once.
|
|
441
|
-
|
|
557
|
+
cachedBlockAttributeTypes = new Map();
|
|
442
558
|
|
|
443
559
|
for ( const blockType of getBlockTypes() as BlockType[] ) {
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
for ( const [ name, definition ] of Object.entries(
|
|
447
|
-
blockType.attributes ?? {}
|
|
448
|
-
) ) {
|
|
449
|
-
if ( 'rich-text' === definition.type ) {
|
|
450
|
-
richTextAttributeMap.set( name, true );
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
cachedRichTextAttributes.set(
|
|
560
|
+
cachedBlockAttributeTypes.set(
|
|
455
561
|
blockType.name,
|
|
456
|
-
|
|
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
|
+
)
|
|
457
570
|
);
|
|
458
571
|
}
|
|
459
572
|
}
|
|
460
573
|
|
|
574
|
+
return cachedBlockAttributeTypes.get( blockName )?.get( attributeName );
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Check if an attribute value is the expected type.
|
|
579
|
+
*
|
|
580
|
+
* @param blockName The name of the block, e.g. 'core/paragraph'.
|
|
581
|
+
* @param attributeName The name of the attribute, e.g. 'content'.
|
|
582
|
+
* @param attributeValue The current attribute value.
|
|
583
|
+
* @return True if the attribute type is expected, false otherwise.
|
|
584
|
+
*/
|
|
585
|
+
function isExpectedAttributeType(
|
|
586
|
+
blockName: string,
|
|
587
|
+
attributeName: string,
|
|
588
|
+
attributeValue: unknown
|
|
589
|
+
): boolean {
|
|
590
|
+
const expectedAttributeType = getBlockAttributeType(
|
|
591
|
+
blockName,
|
|
592
|
+
attributeName
|
|
593
|
+
)?.type;
|
|
594
|
+
|
|
595
|
+
if ( expectedAttributeType === 'rich-text' ) {
|
|
596
|
+
return attributeValue instanceof Y.Text;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if ( expectedAttributeType === 'string' ) {
|
|
600
|
+
return typeof attributeValue === 'string';
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// No other types comparisons use special logic.
|
|
604
|
+
return true;
|
|
605
|
+
}
|
|
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
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Given a block name and attribute key, return true if the attribute is rich-text typed.
|
|
621
|
+
*
|
|
622
|
+
* @param blockName The name of the block, e.g. 'core/paragraph'.
|
|
623
|
+
* @param attributeName The name of the attribute to check, e.g. 'content'.
|
|
624
|
+
* @return True if the attribute is rich-text typed, false otherwise.
|
|
625
|
+
*/
|
|
626
|
+
function isRichTextAttribute(
|
|
627
|
+
blockName: string,
|
|
628
|
+
attributeName: string
|
|
629
|
+
): boolean {
|
|
461
630
|
return (
|
|
462
|
-
|
|
631
|
+
'rich-text' === getBlockAttributeType( blockName, attributeName )?.type
|
|
463
632
|
);
|
|
464
633
|
}
|
|
465
634
|
|
|
@@ -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 ) {
|
|
@@ -88,6 +94,41 @@ function convertYSelectionToBlockSelection(
|
|
|
88
94
|
return null;
|
|
89
95
|
}
|
|
90
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Convert a YFullSelection to a WPSelection by resolving relative positions
|
|
99
|
+
* and verifying the blocks exist in the document.
|
|
100
|
+
* @param yFullSelection The YFullSelection to convert
|
|
101
|
+
* @param ydoc The Y.Doc to resolve positions against
|
|
102
|
+
* @return The converted WPSelection, or null if the conversion fails
|
|
103
|
+
*/
|
|
104
|
+
function convertYFullSelectionToWPSelection(
|
|
105
|
+
yFullSelection: YFullSelection,
|
|
106
|
+
ydoc: Y.Doc
|
|
107
|
+
): WPSelection | null {
|
|
108
|
+
const { start, end } = yFullSelection;
|
|
109
|
+
const startBlock = findBlockByClientIdInDoc( start.clientId, ydoc );
|
|
110
|
+
const endBlock = findBlockByClientIdInDoc( end.clientId, ydoc );
|
|
111
|
+
|
|
112
|
+
if ( ! startBlock || ! endBlock ) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const startBlockSelection = convertYSelectionToBlockSelection(
|
|
117
|
+
start,
|
|
118
|
+
ydoc
|
|
119
|
+
);
|
|
120
|
+
const endBlockSelection = convertYSelectionToBlockSelection( end, ydoc );
|
|
121
|
+
|
|
122
|
+
if ( startBlockSelection === null || endBlockSelection === null ) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
selectionStart: startBlockSelection,
|
|
128
|
+
selectionEnd: endBlockSelection,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
91
132
|
/**
|
|
92
133
|
* Given a Y.Doc and a selection history, find the most recent selection
|
|
93
134
|
* that exists in the document. Skip any selections that are not in the document.
|
|
@@ -99,34 +140,14 @@ function findSelectionFromHistory(
|
|
|
99
140
|
ydoc: Y.Doc,
|
|
100
141
|
selectionHistory: YFullSelection[]
|
|
101
142
|
): WPSelection | null {
|
|
102
|
-
// Try each position until we find one that exists in the document
|
|
103
143
|
for ( const positionToTry of selectionHistory ) {
|
|
104
|
-
const
|
|
105
|
-
|
|
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,
|
|
144
|
+
const result = convertYFullSelectionToWPSelection(
|
|
145
|
+
positionToTry,
|
|
115
146
|
ydoc
|
|
116
147
|
);
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
ydoc
|
|
120
|
-
);
|
|
121
|
-
|
|
122
|
-
if ( startBlockSelection === null || endBlockSelection === null ) {
|
|
123
|
-
continue;
|
|
148
|
+
if ( result !== null ) {
|
|
149
|
+
return result;
|
|
124
150
|
}
|
|
125
|
-
|
|
126
|
-
return {
|
|
127
|
-
selectionStart: startBlockSelection,
|
|
128
|
-
selectionEnd: endBlockSelection,
|
|
129
|
-
};
|
|
130
151
|
}
|
|
131
152
|
|
|
132
153
|
return null;
|
|
@@ -205,3 +226,48 @@ export function restoreSelection(
|
|
|
205
226
|
resetSelection( selectionEnd, selectionEnd, 0 );
|
|
206
227
|
}
|
|
207
228
|
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* If the latest selection has been shifted by remote edits, resolve and return
|
|
232
|
+
* it as a WPSelection. Returns null when the history is empty or neither
|
|
233
|
+
* endpoint has moved.
|
|
234
|
+
*
|
|
235
|
+
* @param ydoc The Y.Doc to resolve positions against
|
|
236
|
+
* @param selectionHistory The selection history to check
|
|
237
|
+
* @return The shifted WPSelection, or null if nothing moved.
|
|
238
|
+
*/
|
|
239
|
+
export function getShiftedSelection(
|
|
240
|
+
ydoc: Y.Doc,
|
|
241
|
+
selectionHistory: YFullSelection[]
|
|
242
|
+
): WPSelection | null {
|
|
243
|
+
if ( selectionHistory.length === 0 ) {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const { start, end } = selectionHistory[ 0 ];
|
|
248
|
+
|
|
249
|
+
// Block-level selections have no offset that can shift.
|
|
250
|
+
if (
|
|
251
|
+
start.type === YSelectionType.BlockSelection ||
|
|
252
|
+
end.type === YSelectionType.BlockSelection
|
|
253
|
+
) {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const selectionStart = convertYSelectionToBlockSelection( start, ydoc );
|
|
258
|
+
const selectionEnd = convertYSelectionToBlockSelection( end, ydoc );
|
|
259
|
+
|
|
260
|
+
if ( ! selectionStart || ! selectionEnd ) {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Only dispatch if at least one endpoint actually moved.
|
|
265
|
+
const startShifted = selectionStart.offset !== start.offset;
|
|
266
|
+
const endShifted = selectionEnd.offset !== end.offset;
|
|
267
|
+
|
|
268
|
+
if ( ! startShifted && ! endShifted ) {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return { selectionStart, selectionEnd };
|
|
273
|
+
}
|
|
@@ -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
|
+
);
|