@wordpress/core-data 7.40.2-next.v.202602241322.0 → 7.41.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.
- package/CHANGELOG.md +2 -0
- package/build/actions.cjs +1 -1
- package/build/actions.cjs.map +2 -2
- package/build/awareness/types.cjs.map +1 -1
- package/build/entities.cjs +17 -10
- package/build/entities.cjs.map +2 -2
- package/build/hooks/use-post-editor-awareness-state.cjs +38 -0
- package/build/hooks/use-post-editor-awareness-state.cjs.map +2 -2
- package/build/private-actions.cjs +7 -2
- package/build/private-actions.cjs.map +2 -2
- package/build/private-apis.cjs +4 -1
- package/build/private-apis.cjs.map +2 -2
- package/build/private-selectors.cjs +7 -2
- package/build/private-selectors.cjs.map +2 -2
- package/build/reducer.cjs +11 -1
- package/build/reducer.cjs.map +2 -2
- package/build/resolvers.cjs +15 -12
- package/build/resolvers.cjs.map +2 -2
- package/build/selectors.cjs.map +2 -2
- package/build/sync.cjs +5 -5
- package/build/sync.cjs.map +1 -1
- package/build/types.cjs.map +1 -1
- package/build/utils/crdt-blocks.cjs +50 -31
- package/build/utils/crdt-blocks.cjs.map +2 -2
- package/build/utils/crdt-selection.cjs +46 -18
- package/build/utils/crdt-selection.cjs.map +2 -2
- package/build/utils/crdt.cjs +12 -1
- package/build/utils/crdt.cjs.map +2 -2
- package/build-module/actions.mjs +1 -1
- package/build-module/actions.mjs.map +2 -2
- package/build-module/entities.mjs +19 -11
- package/build-module/entities.mjs.map +2 -2
- package/build-module/hooks/use-post-editor-awareness-state.mjs +37 -0
- package/build-module/hooks/use-post-editor-awareness-state.mjs.map +2 -2
- package/build-module/private-actions.mjs +5 -1
- package/build-module/private-actions.mjs.map +2 -2
- package/build-module/private-apis.mjs +6 -2
- package/build-module/private-apis.mjs.map +2 -2
- package/build-module/private-selectors.mjs +5 -1
- package/build-module/private-selectors.mjs.map +2 -2
- package/build-module/reducer.mjs +10 -1
- package/build-module/reducer.mjs.map +2 -2
- package/build-module/resolvers.mjs +15 -12
- package/build-module/resolvers.mjs.map +2 -2
- package/build-module/selectors.mjs.map +2 -2
- package/build-module/sync.mjs +3 -3
- package/build-module/sync.mjs.map +1 -1
- package/build-module/utils/crdt-blocks.mjs +50 -31
- package/build-module/utils/crdt-blocks.mjs.map +2 -2
- package/build-module/utils/crdt-selection.mjs +45 -18
- package/build-module/utils/crdt-selection.mjs.map +2 -2
- package/build-module/utils/crdt.mjs +16 -6
- package/build-module/utils/crdt.mjs.map +2 -2
- package/build-types/awareness/types.d.ts +5 -0
- package/build-types/awareness/types.d.ts.map +1 -1
- package/build-types/entities.d.ts +1 -1
- package/build-types/entities.d.ts.map +1 -1
- package/build-types/hooks/use-post-editor-awareness-state.d.ts +10 -1
- package/build-types/hooks/use-post-editor-awareness-state.d.ts.map +1 -1
- package/build-types/index.d.ts.map +1 -1
- package/build-types/private-actions.d.ts +1 -0
- package/build-types/private-actions.d.ts.map +1 -1
- package/build-types/private-apis.d.ts.map +1 -1
- package/build-types/private-selectors.d.ts +7 -0
- package/build-types/private-selectors.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 -0
- 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 +1 -0
- package/build-types/types.d.ts.map +1 -1
- package/build-types/utils/crdt-blocks.d.ts +1 -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.d.ts +1 -0
- package/build-types/utils/crdt.d.ts.map +1 -1
- package/package.json +18 -18
- package/src/actions.js +2 -2
- package/src/awareness/types.ts +6 -0
- package/src/entities.js +23 -11
- package/src/hooks/use-post-editor-awareness-state.ts +70 -0
- package/src/private-actions.js +13 -0
- package/src/private-apis.js +4 -0
- package/src/private-selectors.ts +10 -0
- package/src/reducer.js +21 -0
- package/src/resolvers.js +21 -15
- package/src/selectors.ts +1 -0
- package/src/sync.ts +2 -2
- package/src/test/entities.js +47 -14
- package/src/test/resolvers.js +46 -80
- package/src/types.ts +1 -0
- package/src/utils/crdt-blocks.ts +113 -47
- package/src/utils/crdt-selection.ts +84 -24
- package/src/utils/crdt.ts +23 -7
- package/src/utils/test/crdt-blocks.ts +938 -0
- package/src/utils/test/crdt.ts +136 -10
package/src/test/resolvers.js
CHANGED
|
@@ -832,8 +832,8 @@ describe( 'canUser', () => {
|
|
|
832
832
|
batch: ( callback ) => callback(),
|
|
833
833
|
};
|
|
834
834
|
dispatch = Object.assign( jest.fn(), {
|
|
835
|
-
|
|
836
|
-
|
|
835
|
+
receiveUserPermissions: jest.fn(),
|
|
836
|
+
finishResolutions: jest.fn(),
|
|
837
837
|
} );
|
|
838
838
|
triggerFetch.mockReset();
|
|
839
839
|
} );
|
|
@@ -859,7 +859,7 @@ describe( 'canUser', () => {
|
|
|
859
859
|
parse: false,
|
|
860
860
|
} );
|
|
861
861
|
|
|
862
|
-
expect( dispatch.
|
|
862
|
+
expect( dispatch.receiveUserPermissions ).not.toHaveBeenCalled();
|
|
863
863
|
} );
|
|
864
864
|
|
|
865
865
|
it( 'throws an error when an entity resource object is malformed', async () => {
|
|
@@ -888,9 +888,8 @@ describe( 'canUser', () => {
|
|
|
888
888
|
parse: false,
|
|
889
889
|
} );
|
|
890
890
|
|
|
891
|
-
expect( dispatch.
|
|
892
|
-
'create/media'
|
|
893
|
-
false
|
|
891
|
+
expect( dispatch.receiveUserPermissions ).toHaveBeenCalledWith(
|
|
892
|
+
expect.objectContaining( { 'create/media': false } )
|
|
894
893
|
);
|
|
895
894
|
} );
|
|
896
895
|
|
|
@@ -911,9 +910,8 @@ describe( 'canUser', () => {
|
|
|
911
910
|
parse: false,
|
|
912
911
|
} );
|
|
913
912
|
|
|
914
|
-
expect( dispatch.
|
|
915
|
-
'create/postType/attachment'
|
|
916
|
-
false
|
|
913
|
+
expect( dispatch.receiveUserPermissions ).toHaveBeenCalledWith(
|
|
914
|
+
expect.objectContaining( { 'create/postType/attachment': false } )
|
|
917
915
|
);
|
|
918
916
|
} );
|
|
919
917
|
|
|
@@ -933,9 +931,8 @@ describe( 'canUser', () => {
|
|
|
933
931
|
parse: false,
|
|
934
932
|
} );
|
|
935
933
|
|
|
936
|
-
expect( dispatch.
|
|
937
|
-
'create/media'
|
|
938
|
-
true
|
|
934
|
+
expect( dispatch.receiveUserPermissions ).toHaveBeenCalledWith(
|
|
935
|
+
expect.objectContaining( { 'create/media': true } )
|
|
939
936
|
);
|
|
940
937
|
} );
|
|
941
938
|
|
|
@@ -956,9 +953,8 @@ describe( 'canUser', () => {
|
|
|
956
953
|
parse: false,
|
|
957
954
|
} );
|
|
958
955
|
|
|
959
|
-
expect( dispatch.
|
|
960
|
-
'create/postType/attachment'
|
|
961
|
-
true
|
|
956
|
+
expect( dispatch.receiveUserPermissions ).toHaveBeenCalledWith(
|
|
957
|
+
expect.objectContaining( { 'create/postType/attachment': true } )
|
|
962
958
|
);
|
|
963
959
|
} );
|
|
964
960
|
|
|
@@ -979,9 +975,8 @@ describe( 'canUser', () => {
|
|
|
979
975
|
parse: false,
|
|
980
976
|
} );
|
|
981
977
|
|
|
982
|
-
expect( dispatch.
|
|
983
|
-
'create/blocks/123'
|
|
984
|
-
true
|
|
978
|
+
expect( dispatch.receiveUserPermissions ).toHaveBeenCalledWith(
|
|
979
|
+
expect.objectContaining( { 'create/blocks/123': true } )
|
|
985
980
|
);
|
|
986
981
|
} );
|
|
987
982
|
|
|
@@ -1006,9 +1001,8 @@ describe( 'canUser', () => {
|
|
|
1006
1001
|
parse: false,
|
|
1007
1002
|
} );
|
|
1008
1003
|
|
|
1009
|
-
expect( dispatch.
|
|
1010
|
-
'create/postType/wp_block/123'
|
|
1011
|
-
true
|
|
1004
|
+
expect( dispatch.receiveUserPermissions ).toHaveBeenCalledWith(
|
|
1005
|
+
expect.objectContaining( { 'create/postType/wp_block/123': true } )
|
|
1012
1006
|
);
|
|
1013
1007
|
} );
|
|
1014
1008
|
|
|
@@ -1035,13 +1029,11 @@ describe( 'canUser', () => {
|
|
|
1035
1029
|
|
|
1036
1030
|
expect( triggerFetch ).toHaveBeenCalledTimes( 1 );
|
|
1037
1031
|
|
|
1038
|
-
expect( dispatch.
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
'read/blocks',
|
|
1044
|
-
true
|
|
1032
|
+
expect( dispatch.receiveUserPermissions ).toHaveBeenCalledWith(
|
|
1033
|
+
expect.objectContaining( {
|
|
1034
|
+
'create/blocks': true,
|
|
1035
|
+
'read/blocks': true,
|
|
1036
|
+
} )
|
|
1045
1037
|
);
|
|
1046
1038
|
} );
|
|
1047
1039
|
|
|
@@ -1076,13 +1068,11 @@ describe( 'canUser', () => {
|
|
|
1076
1068
|
|
|
1077
1069
|
expect( triggerFetch ).toHaveBeenCalledTimes( 1 );
|
|
1078
1070
|
|
|
1079
|
-
expect( dispatch.
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
'read/postType/wp_block',
|
|
1085
|
-
true
|
|
1071
|
+
expect( dispatch.receiveUserPermissions ).toHaveBeenCalledWith(
|
|
1072
|
+
expect.objectContaining( {
|
|
1073
|
+
'create/postType/wp_block': true,
|
|
1074
|
+
'read/postType/wp_block': true,
|
|
1075
|
+
} )
|
|
1086
1076
|
);
|
|
1087
1077
|
} );
|
|
1088
1078
|
|
|
@@ -1115,21 +1105,13 @@ describe( 'canUser', () => {
|
|
|
1115
1105
|
'blocks'
|
|
1116
1106
|
)( { dispatch, registry, resolveSelect } );
|
|
1117
1107
|
|
|
1118
|
-
expect( dispatch.
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
);
|
|
1126
|
-
expect( dispatch.receiveUserPermission ).toHaveBeenCalledWith(
|
|
1127
|
-
'update/blocks',
|
|
1128
|
-
false
|
|
1129
|
-
);
|
|
1130
|
-
expect( dispatch.receiveUserPermission ).toHaveBeenCalledWith(
|
|
1131
|
-
'delete/blocks',
|
|
1132
|
-
false
|
|
1108
|
+
expect( dispatch.receiveUserPermissions ).toHaveBeenCalledWith(
|
|
1109
|
+
expect.objectContaining( {
|
|
1110
|
+
'create/blocks': true,
|
|
1111
|
+
'read/blocks': true,
|
|
1112
|
+
'update/blocks': false,
|
|
1113
|
+
'delete/blocks': false,
|
|
1114
|
+
} )
|
|
1133
1115
|
);
|
|
1134
1116
|
} );
|
|
1135
1117
|
|
|
@@ -1168,21 +1150,13 @@ describe( 'canUser', () => {
|
|
|
1168
1150
|
|
|
1169
1151
|
expect( triggerFetch ).toHaveBeenCalledTimes( 1 );
|
|
1170
1152
|
|
|
1171
|
-
expect( dispatch.
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
);
|
|
1179
|
-
expect( dispatch.receiveUserPermission ).toHaveBeenCalledWith(
|
|
1180
|
-
'update/blocks/123',
|
|
1181
|
-
true
|
|
1182
|
-
);
|
|
1183
|
-
expect( dispatch.receiveUserPermission ).toHaveBeenCalledWith(
|
|
1184
|
-
'delete/blocks/123',
|
|
1185
|
-
true
|
|
1153
|
+
expect( dispatch.receiveUserPermissions ).toHaveBeenCalledWith(
|
|
1154
|
+
expect.objectContaining( {
|
|
1155
|
+
'create/blocks/123': true,
|
|
1156
|
+
'read/blocks/123': true,
|
|
1157
|
+
'update/blocks/123': true,
|
|
1158
|
+
'delete/blocks/123': true,
|
|
1159
|
+
} )
|
|
1186
1160
|
);
|
|
1187
1161
|
} );
|
|
1188
1162
|
|
|
@@ -1221,21 +1195,13 @@ describe( 'canUser', () => {
|
|
|
1221
1195
|
|
|
1222
1196
|
expect( triggerFetch ).toHaveBeenCalledTimes( 1 );
|
|
1223
1197
|
|
|
1224
|
-
expect( dispatch.
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
);
|
|
1232
|
-
expect( dispatch.receiveUserPermission ).toHaveBeenCalledWith(
|
|
1233
|
-
'update/postType/wp_block/123',
|
|
1234
|
-
true
|
|
1235
|
-
);
|
|
1236
|
-
expect( dispatch.receiveUserPermission ).toHaveBeenCalledWith(
|
|
1237
|
-
'delete/postType/wp_block/123',
|
|
1238
|
-
true
|
|
1198
|
+
expect( dispatch.receiveUserPermissions ).toHaveBeenCalledWith(
|
|
1199
|
+
expect.objectContaining( {
|
|
1200
|
+
'create/postType/wp_block/123': true,
|
|
1201
|
+
'read/postType/wp_block/123': true,
|
|
1202
|
+
'update/postType/wp_block/123': true,
|
|
1203
|
+
'delete/postType/wp_block/123': true,
|
|
1204
|
+
} )
|
|
1239
1205
|
);
|
|
1240
1206
|
} );
|
|
1241
1207
|
} );
|
package/src/types.ts
CHANGED
package/src/utils/crdt-blocks.ts
CHANGED
|
@@ -39,7 +39,7 @@ export interface Block {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
// A block as represented in the CRDT document (Y.Map).
|
|
42
|
-
interface YBlockRecord extends YMapRecord {
|
|
42
|
+
export interface YBlockRecord extends YMapRecord {
|
|
43
43
|
attributes: YBlockAttributes;
|
|
44
44
|
clientId: string;
|
|
45
45
|
innerBlocks: YBlocks;
|
|
@@ -283,43 +283,29 @@ export function mergeCrdtBlocks(
|
|
|
283
283
|
|
|
284
284
|
Object.entries( value ).forEach(
|
|
285
285
|
( [ attributeName, attributeValue ] ) => {
|
|
286
|
-
if (
|
|
287
|
-
fastDeepEqual(
|
|
288
|
-
currentAttributes?.get( attributeName ),
|
|
289
|
-
attributeValue
|
|
290
|
-
)
|
|
291
|
-
) {
|
|
292
|
-
return;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
286
|
const currentAttribute =
|
|
296
|
-
currentAttributes
|
|
297
|
-
|
|
287
|
+
currentAttributes?.get( attributeName );
|
|
288
|
+
|
|
289
|
+
const isExpectedType = isExpectedAttributeType(
|
|
298
290
|
block.name,
|
|
299
|
-
attributeName
|
|
291
|
+
attributeName,
|
|
292
|
+
currentAttribute
|
|
300
293
|
);
|
|
301
294
|
|
|
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(
|
|
295
|
+
const isAttributeChanged =
|
|
296
|
+
! isExpectedType ||
|
|
297
|
+
! fastDeepEqual(
|
|
311
298
|
currentAttribute,
|
|
312
|
-
attributeValue
|
|
313
|
-
cursorPosition
|
|
299
|
+
attributeValue
|
|
314
300
|
);
|
|
315
|
-
|
|
316
|
-
|
|
301
|
+
|
|
302
|
+
if ( isAttributeChanged ) {
|
|
303
|
+
updateYBlockAttribute(
|
|
304
|
+
block.name,
|
|
317
305
|
attributeName,
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
attributeValue
|
|
322
|
-
)
|
|
306
|
+
attributeValue,
|
|
307
|
+
currentAttributes,
|
|
308
|
+
cursorPosition
|
|
323
309
|
);
|
|
324
310
|
}
|
|
325
311
|
}
|
|
@@ -422,45 +408,125 @@ function shouldBlockBeSynced( block: Block ): boolean {
|
|
|
422
408
|
return true;
|
|
423
409
|
}
|
|
424
410
|
|
|
425
|
-
|
|
426
|
-
|
|
411
|
+
/**
|
|
412
|
+
* Update a single attribute on a Yjs block attributes map (currentAttributes).
|
|
413
|
+
*
|
|
414
|
+
* For rich-text attributes that already exist as Y.Text instances, the update
|
|
415
|
+
* is applied as a delta merge so that concurrent edits are preserved. All
|
|
416
|
+
* other attributes are replaced wholesale via `createNewYAttributeValue`.
|
|
417
|
+
*
|
|
418
|
+
* @param blockName The block type name, e.g. 'core/paragraph'.
|
|
419
|
+
* @param attributeName The name of the attribute to update, e.g. 'content'.
|
|
420
|
+
* @param attributeValue The new value for the attribute.
|
|
421
|
+
* @param currentAttributes The Y.Map holding the block's current attributes.
|
|
422
|
+
* @param cursorPosition The local cursor position, used when merging rich-text deltas.
|
|
423
|
+
*/
|
|
424
|
+
function updateYBlockAttribute(
|
|
425
|
+
blockName: string,
|
|
426
|
+
attributeName: string,
|
|
427
|
+
attributeValue: unknown,
|
|
428
|
+
currentAttributes: YBlockAttributes,
|
|
429
|
+
cursorPosition: number | null
|
|
430
|
+
): void {
|
|
431
|
+
const isRichText = isRichTextAttribute( blockName, attributeName );
|
|
432
|
+
const currentAttribute = currentAttributes.get( attributeName );
|
|
433
|
+
|
|
434
|
+
if (
|
|
435
|
+
isRichText &&
|
|
436
|
+
'string' === typeof attributeValue &&
|
|
437
|
+
currentAttributes.has( attributeName ) &&
|
|
438
|
+
currentAttribute instanceof Y.Text
|
|
439
|
+
) {
|
|
440
|
+
// Rich text values are stored as persistent Y.Text instances.
|
|
441
|
+
// Update the value with a delta in place.
|
|
442
|
+
mergeRichTextUpdate( currentAttribute, attributeValue, cursorPosition );
|
|
443
|
+
} else {
|
|
444
|
+
currentAttributes.set(
|
|
445
|
+
attributeName,
|
|
446
|
+
createNewYAttributeValue( blockName, attributeName, attributeValue )
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Cached types for block attributes.
|
|
452
|
+
let cachedBlockAttributeTypes: Map< string, Map< string, string > >;
|
|
427
453
|
|
|
428
454
|
/**
|
|
429
|
-
*
|
|
455
|
+
* Get the defined attribute type for a block attribute.
|
|
430
456
|
*
|
|
431
457
|
* @param blockName The name of the block, e.g. 'core/paragraph'.
|
|
432
|
-
* @param attributeName The name of the attribute
|
|
433
|
-
* @return
|
|
458
|
+
* @param attributeName The name of the attribute, e.g. 'content'.
|
|
459
|
+
* @return The type of the attribute, e.g. 'rich-text' or 'string'.
|
|
434
460
|
*/
|
|
435
|
-
function
|
|
461
|
+
function getBlockAttributeType(
|
|
436
462
|
blockName: string,
|
|
437
463
|
attributeName: string
|
|
438
|
-
):
|
|
439
|
-
if ( !
|
|
464
|
+
): string | undefined {
|
|
465
|
+
if ( ! cachedBlockAttributeTypes ) {
|
|
440
466
|
// Parse the attributes for all blocks once.
|
|
441
|
-
|
|
467
|
+
cachedBlockAttributeTypes = new Map< string, Map< string, string > >();
|
|
442
468
|
|
|
443
469
|
for ( const blockType of getBlockTypes() as BlockType[] ) {
|
|
444
|
-
const
|
|
470
|
+
const blockAttributeTypeMap = new Map< string, string >();
|
|
445
471
|
|
|
446
472
|
for ( const [ name, definition ] of Object.entries(
|
|
447
473
|
blockType.attributes ?? {}
|
|
448
474
|
) ) {
|
|
449
|
-
if (
|
|
450
|
-
|
|
475
|
+
if ( definition.type ) {
|
|
476
|
+
blockAttributeTypeMap.set( name, definition.type );
|
|
451
477
|
}
|
|
452
478
|
}
|
|
453
479
|
|
|
454
|
-
|
|
480
|
+
cachedBlockAttributeTypes.set(
|
|
455
481
|
blockType.name,
|
|
456
|
-
|
|
482
|
+
blockAttributeTypeMap
|
|
457
483
|
);
|
|
458
484
|
}
|
|
459
485
|
}
|
|
460
486
|
|
|
461
|
-
return (
|
|
462
|
-
|
|
487
|
+
return cachedBlockAttributeTypes.get( blockName )?.get( attributeName );
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Check if an attribute value is the expected type.
|
|
492
|
+
*
|
|
493
|
+
* @param blockName The name of the block, e.g. 'core/paragraph'.
|
|
494
|
+
* @param attributeName The name of the attribute, e.g. 'content'.
|
|
495
|
+
* @param attributeValue The current attribute value.
|
|
496
|
+
* @return True if the attribute type is expected, false otherwise.
|
|
497
|
+
*/
|
|
498
|
+
function isExpectedAttributeType(
|
|
499
|
+
blockName: string,
|
|
500
|
+
attributeName: string,
|
|
501
|
+
attributeValue: unknown
|
|
502
|
+
): boolean {
|
|
503
|
+
const expectedAttributeType = getBlockAttributeType(
|
|
504
|
+
blockName,
|
|
505
|
+
attributeName
|
|
463
506
|
);
|
|
507
|
+
|
|
508
|
+
if ( expectedAttributeType === 'rich-text' ) {
|
|
509
|
+
return attributeValue instanceof Y.Text;
|
|
510
|
+
} else if ( expectedAttributeType === 'string' ) {
|
|
511
|
+
return typeof attributeValue === 'string';
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// No other types comparisons use special logic.
|
|
515
|
+
return true;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Given a block name and attribute key, return true if the attribute is rich-text typed.
|
|
520
|
+
*
|
|
521
|
+
* @param blockName The name of the block, e.g. 'core/paragraph'.
|
|
522
|
+
* @param attributeName The name of the attribute to check, e.g. 'content'.
|
|
523
|
+
* @return True if the attribute is rich-text typed, false otherwise.
|
|
524
|
+
*/
|
|
525
|
+
function isRichTextAttribute(
|
|
526
|
+
blockName: string,
|
|
527
|
+
attributeName: string
|
|
528
|
+
): boolean {
|
|
529
|
+
return 'rich-text' === getBlockAttributeType( blockName, attributeName );
|
|
464
530
|
}
|
|
465
531
|
|
|
466
532
|
let localDoc: Y.Doc;
|
|
@@ -88,6 +88,41 @@ function convertYSelectionToBlockSelection(
|
|
|
88
88
|
return null;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Convert a YFullSelection to a WPSelection by resolving relative positions
|
|
93
|
+
* and verifying the blocks exist in the document.
|
|
94
|
+
* @param yFullSelection The YFullSelection to convert
|
|
95
|
+
* @param ydoc The Y.Doc to resolve positions against
|
|
96
|
+
* @return The converted WPSelection, or null if the conversion fails
|
|
97
|
+
*/
|
|
98
|
+
function convertYFullSelectionToWPSelection(
|
|
99
|
+
yFullSelection: YFullSelection,
|
|
100
|
+
ydoc: Y.Doc
|
|
101
|
+
): WPSelection | null {
|
|
102
|
+
const { start, end } = yFullSelection;
|
|
103
|
+
const startBlock = findBlockByClientIdInDoc( start.clientId, ydoc );
|
|
104
|
+
const endBlock = findBlockByClientIdInDoc( end.clientId, ydoc );
|
|
105
|
+
|
|
106
|
+
if ( ! startBlock || ! endBlock ) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const startBlockSelection = convertYSelectionToBlockSelection(
|
|
111
|
+
start,
|
|
112
|
+
ydoc
|
|
113
|
+
);
|
|
114
|
+
const endBlockSelection = convertYSelectionToBlockSelection( end, ydoc );
|
|
115
|
+
|
|
116
|
+
if ( startBlockSelection === null || endBlockSelection === null ) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
selectionStart: startBlockSelection,
|
|
122
|
+
selectionEnd: endBlockSelection,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
91
126
|
/**
|
|
92
127
|
* Given a Y.Doc and a selection history, find the most recent selection
|
|
93
128
|
* that exists in the document. Skip any selections that are not in the document.
|
|
@@ -99,34 +134,14 @@ function findSelectionFromHistory(
|
|
|
99
134
|
ydoc: Y.Doc,
|
|
100
135
|
selectionHistory: YFullSelection[]
|
|
101
136
|
): WPSelection | null {
|
|
102
|
-
// Try each position until we find one that exists in the document
|
|
103
137
|
for ( const positionToTry of selectionHistory ) {
|
|
104
|
-
const
|
|
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,
|
|
138
|
+
const result = convertYFullSelectionToWPSelection(
|
|
139
|
+
positionToTry,
|
|
115
140
|
ydoc
|
|
116
141
|
);
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
ydoc
|
|
120
|
-
);
|
|
121
|
-
|
|
122
|
-
if ( startBlockSelection === null || endBlockSelection === null ) {
|
|
123
|
-
continue;
|
|
142
|
+
if ( result !== null ) {
|
|
143
|
+
return result;
|
|
124
144
|
}
|
|
125
|
-
|
|
126
|
-
return {
|
|
127
|
-
selectionStart: startBlockSelection,
|
|
128
|
-
selectionEnd: endBlockSelection,
|
|
129
|
-
};
|
|
130
145
|
}
|
|
131
146
|
|
|
132
147
|
return null;
|
|
@@ -205,3 +220,48 @@ export function restoreSelection(
|
|
|
205
220
|
resetSelection( selectionEnd, selectionEnd, 0 );
|
|
206
221
|
}
|
|
207
222
|
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* If the latest selection has been shifted by remote edits, resolve and return
|
|
226
|
+
* it as a WPSelection. Returns null when the history is empty or neither
|
|
227
|
+
* endpoint has moved.
|
|
228
|
+
*
|
|
229
|
+
* @param ydoc The Y.Doc to resolve positions against
|
|
230
|
+
* @param selectionHistory The selection history to check
|
|
231
|
+
* @return The shifted WPSelection, or null if nothing moved.
|
|
232
|
+
*/
|
|
233
|
+
export function getShiftedSelection(
|
|
234
|
+
ydoc: Y.Doc,
|
|
235
|
+
selectionHistory: YFullSelection[]
|
|
236
|
+
): WPSelection | null {
|
|
237
|
+
if ( selectionHistory.length === 0 ) {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const { start, end } = selectionHistory[ 0 ];
|
|
242
|
+
|
|
243
|
+
// Block-level selections have no offset that can shift.
|
|
244
|
+
if (
|
|
245
|
+
start.type === YSelectionType.BlockSelection ||
|
|
246
|
+
end.type === YSelectionType.BlockSelection
|
|
247
|
+
) {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const selectionStart = convertYSelectionToBlockSelection( start, ydoc );
|
|
252
|
+
const selectionEnd = convertYSelectionToBlockSelection( end, ydoc );
|
|
253
|
+
|
|
254
|
+
if ( ! selectionStart || ! selectionEnd ) {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Only dispatch if at least one endpoint actually moved.
|
|
259
|
+
const startShifted = selectionStart.offset !== start.offset;
|
|
260
|
+
const endShifted = selectionEnd.offset !== end.offset;
|
|
261
|
+
|
|
262
|
+
if ( ! startShifted && ! endShifted ) {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return { selectionStart, selectionEnd };
|
|
267
|
+
}
|
package/src/utils/crdt.ts
CHANGED
|
@@ -28,13 +28,13 @@ import {
|
|
|
28
28
|
} from './crdt-blocks';
|
|
29
29
|
import { type Post } from '../entity-types/post';
|
|
30
30
|
import { type Type } from '../entity-types';
|
|
31
|
-
import {
|
|
32
|
-
CRDT_DOC_META_PERSISTENCE_KEY,
|
|
33
|
-
CRDT_RECORD_MAP_KEY,
|
|
34
|
-
WORDPRESS_META_KEY_FOR_CRDT_DOC_PERSISTENCE,
|
|
35
|
-
} from '../sync';
|
|
31
|
+
import { CRDT_DOC_META_PERSISTENCE_KEY, CRDT_RECORD_MAP_KEY } from '../sync';
|
|
36
32
|
import type { WPSelection } from '../types';
|
|
37
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
getSelectionHistory,
|
|
35
|
+
getShiftedSelection,
|
|
36
|
+
updateSelectionHistory,
|
|
37
|
+
} from './crdt-selection';
|
|
38
38
|
import {
|
|
39
39
|
createYMap,
|
|
40
40
|
getRootMap,
|
|
@@ -74,6 +74,8 @@ export interface YPostRecord extends YMapRecord {
|
|
|
74
74
|
title: Y.Text;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
export const POST_META_KEY_FOR_CRDT_DOC_PERSISTENCE = '_crdt_document';
|
|
78
|
+
|
|
77
79
|
// Properties that are allowed to be synced for a post.
|
|
78
80
|
const allowedPostProperties = new Set< string >( [
|
|
79
81
|
'author',
|
|
@@ -97,7 +99,7 @@ const allowedPostProperties = new Set< string >( [
|
|
|
97
99
|
|
|
98
100
|
// Post meta keys that should *not* be synced.
|
|
99
101
|
const disallowedPostMetaKeys = new Set< string >( [
|
|
100
|
-
|
|
102
|
+
POST_META_KEY_FOR_CRDT_DOC_PERSISTENCE,
|
|
101
103
|
] );
|
|
102
104
|
|
|
103
105
|
/**
|
|
@@ -416,6 +418,20 @@ export function getPostChangesFromCRDTDoc(
|
|
|
416
418
|
};
|
|
417
419
|
}
|
|
418
420
|
|
|
421
|
+
// When remote content changes are detected, recalculate the local user's
|
|
422
|
+
// selection using Y.RelativePosition to account for text shifts. The ydoc
|
|
423
|
+
// has already been updated with remote content at this point, so converting
|
|
424
|
+
// relative positions to absolute gives corrected offsets. Including the
|
|
425
|
+
// selection in PostChanges ensures it dispatches atomically with content.
|
|
426
|
+
const selectionHistory = getSelectionHistory( ydoc );
|
|
427
|
+
const shiftedSelection = getShiftedSelection( ydoc, selectionHistory );
|
|
428
|
+
if ( shiftedSelection ) {
|
|
429
|
+
changes.selection = {
|
|
430
|
+
...shiftedSelection,
|
|
431
|
+
initialPosition: 0,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
419
435
|
return changes;
|
|
420
436
|
}
|
|
421
437
|
|