@wordpress/core-data 7.40.1 → 7.40.2-next.v.202602241322.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/build/actions.cjs +23 -29
- package/build/actions.cjs.map +2 -2
- package/build/awareness/block-lookup.cjs +103 -0
- package/build/awareness/block-lookup.cjs.map +7 -0
- package/build/awareness/post-editor-awareness.cjs +45 -7
- package/build/awareness/post-editor-awareness.cjs.map +3 -3
- package/build/entities.cjs +60 -63
- package/build/entities.cjs.map +2 -2
- package/build/entity-types/icon.cjs +19 -0
- package/build/entity-types/icon.cjs.map +7 -0
- package/build/entity-types/index.cjs.map +1 -1
- package/build/hooks/use-post-editor-awareness-state.cjs +12 -8
- package/build/hooks/use-post-editor-awareness-state.cjs.map +2 -2
- package/build/private-actions.cjs +0 -8
- package/build/private-actions.cjs.map +2 -2
- package/build/private-apis.cjs +1 -1
- package/build/private-apis.cjs.map +1 -1
- package/build/private-selectors.cjs +1 -9
- package/build/private-selectors.cjs.map +2 -2
- package/build/reducer.cjs +0 -10
- package/build/reducer.cjs.map +2 -2
- package/build/resolvers.cjs +101 -113
- package/build/resolvers.cjs.map +2 -2
- package/build/selectors.cjs.map +2 -2
- package/build/sync.cjs +0 -3
- package/build/sync.cjs.map +2 -2
- package/build/types.cjs.map +1 -1
- package/build/utils/crdt-selection.cjs +1 -1
- package/build/utils/crdt-selection.cjs.map +2 -2
- package/build/utils/crdt-user-selections.cjs +78 -22
- package/build/utils/crdt-user-selections.cjs.map +3 -3
- package/build-module/actions.mjs +23 -29
- package/build-module/actions.mjs.map +2 -2
- package/build-module/awareness/block-lookup.mjs +77 -0
- package/build-module/awareness/block-lookup.mjs.map +7 -0
- package/build-module/awareness/post-editor-awareness.mjs +47 -8
- package/build-module/awareness/post-editor-awareness.mjs.map +3 -3
- package/build-module/entities.mjs +60 -63
- package/build-module/entities.mjs.map +2 -2
- package/build-module/entity-types/icon.mjs +1 -0
- package/build-module/entity-types/icon.mjs.map +7 -0
- package/build-module/hooks/use-post-editor-awareness-state.mjs +10 -6
- package/build-module/hooks/use-post-editor-awareness-state.mjs.map +2 -2
- package/build-module/private-actions.mjs +0 -7
- package/build-module/private-actions.mjs.map +2 -2
- package/build-module/private-apis.mjs +2 -2
- package/build-module/private-apis.mjs.map +1 -1
- package/build-module/private-selectors.mjs +2 -12
- package/build-module/private-selectors.mjs.map +2 -2
- package/build-module/reducer.mjs +0 -9
- package/build-module/reducer.mjs.map +2 -2
- package/build-module/resolvers.mjs +101 -112
- package/build-module/resolvers.mjs.map +2 -2
- package/build-module/selectors.mjs.map +2 -2
- package/build-module/sync.mjs +0 -2
- package/build-module/sync.mjs.map +2 -2
- package/build-module/utils/crdt-selection.mjs +1 -1
- package/build-module/utils/crdt-selection.mjs.map +2 -2
- package/build-module/utils/crdt-user-selections.mjs +77 -22
- package/build-module/utils/crdt-user-selections.mjs.map +2 -2
- package/build-types/actions.d.ts.map +1 -1
- package/build-types/awareness/block-lookup.d.ts +29 -0
- package/build-types/awareness/block-lookup.d.ts.map +1 -0
- package/build-types/awareness/post-editor-awareness.d.ts +18 -5
- package/build-types/awareness/post-editor-awareness.d.ts.map +1 -1
- package/build-types/awareness/test/block-lookup.d.ts +2 -0
- package/build-types/awareness/test/block-lookup.d.ts.map +1 -0
- package/build-types/entities.d.ts +16 -0
- package/build-types/entities.d.ts.map +1 -1
- package/build-types/entity-types/icon.d.ts +25 -0
- package/build-types/entity-types/icon.d.ts.map +1 -0
- package/build-types/entity-types/index.d.ts +3 -2
- package/build-types/entity-types/index.d.ts.map +1 -1
- package/build-types/hooks/use-post-editor-awareness-state.d.ts +11 -6
- 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 +0 -8
- package/build-types/private-actions.d.ts.map +1 -1
- package/build-types/private-selectors.d.ts +1 -8
- package/build-types/private-selectors.d.ts.map +1 -1
- package/build-types/reducer.d.ts +0 -11
- package/build-types/reducer.d.ts.map +1 -1
- package/build-types/resolvers.d.ts +0 -3
- package/build-types/resolvers.d.ts.map +1 -1
- package/build-types/selectors.d.ts +0 -6
- 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 +13 -5
- package/build-types/types.d.ts.map +1 -1
- package/build-types/utils/crdt-selection.d.ts.map +1 -1
- package/build-types/utils/crdt-user-selections.d.ts +21 -4
- package/build-types/utils/crdt-user-selections.d.ts.map +1 -1
- package/build-types/utils/test/crdt-user-selections.d.ts +2 -0
- package/build-types/utils/test/crdt-user-selections.d.ts.map +1 -0
- package/package.json +18 -18
- package/src/actions.js +39 -45
- package/src/awareness/block-lookup.ts +169 -0
- package/src/awareness/post-editor-awareness.ts +68 -11
- package/src/awareness/test/block-lookup.ts +504 -0
- package/src/awareness/test/post-editor-awareness.ts +662 -38
- package/src/entities.js +63 -66
- package/src/entity-types/icon.ts +30 -0
- package/src/entity-types/index.ts +3 -0
- package/src/hooks/test/use-post-editor-awareness-state.ts +21 -14
- package/src/hooks/use-post-editor-awareness-state.ts +22 -13
- package/src/private-actions.js +0 -14
- package/src/private-apis.js +2 -2
- package/src/private-selectors.ts +3 -22
- package/src/reducer.js +0 -17
- package/src/resolvers.js +137 -156
- package/src/selectors.ts +0 -7
- package/src/sync.ts +0 -2
- package/src/test/resolvers.js +109 -1
- package/src/types.ts +22 -5
- package/src/utils/crdt-selection.ts +3 -1
- package/src/utils/crdt-user-selections.ts +129 -47
- package/src/utils/test/crdt-user-selections.ts +894 -0
|
@@ -9,7 +9,11 @@ import { dispatch, select, subscribe, resolveSelect } from '@wordpress/data';
|
|
|
9
9
|
*/
|
|
10
10
|
import { PostEditorAwareness } from '../post-editor-awareness';
|
|
11
11
|
import { SelectionType } from '../../utils/crdt-user-selections';
|
|
12
|
-
import type {
|
|
12
|
+
import type {
|
|
13
|
+
SelectionNone,
|
|
14
|
+
SelectionCursor,
|
|
15
|
+
SelectionWholeBlock,
|
|
16
|
+
} from '../../types';
|
|
13
17
|
import { CRDT_RECORD_MAP_KEY } from '../../sync';
|
|
14
18
|
import type { CollaboratorInfo } from '../types';
|
|
15
19
|
|
|
@@ -46,23 +50,117 @@ const createMockUser = () => ( {
|
|
|
46
50
|
avatar_urls: mockAvatarUrls,
|
|
47
51
|
} );
|
|
48
52
|
|
|
53
|
+
type MockBlock = {
|
|
54
|
+
clientId: string;
|
|
55
|
+
name?: string;
|
|
56
|
+
innerBlocks: MockBlock[];
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
interface MockBlockEditorOverrides {
|
|
60
|
+
blocks?: MockBlock[];
|
|
61
|
+
getBlocks?: jest.Mock;
|
|
62
|
+
getBlockName?: string;
|
|
63
|
+
getSelectionStart?: jest.Mock;
|
|
64
|
+
getSelectionEnd?: jest.Mock;
|
|
65
|
+
}
|
|
66
|
+
|
|
49
67
|
/**
|
|
50
|
-
*
|
|
68
|
+
* Mock the block-editor store selectors returned by `select( blockEditorStore )`.
|
|
69
|
+
*
|
|
70
|
+
* Only the fields that vary between tests need to be passed — everything else
|
|
71
|
+
* gets sensible defaults. Pass `blocks` for the common case (static return
|
|
72
|
+
* value) or `getBlocks` when you need `mockImplementation` (e.g. template mode).
|
|
73
|
+
*
|
|
74
|
+
* Returns `{ getBlocks }` so callers can assert on it (e.g. `toHaveBeenCalledWith`).
|
|
75
|
+
*
|
|
76
|
+
* @param overrides - Optional selector overrides.
|
|
51
77
|
*/
|
|
52
|
-
function
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
78
|
+
function mockBlockEditorStore( overrides: MockBlockEditorOverrides = {} ) {
|
|
79
|
+
const defaultBlocks = [
|
|
80
|
+
{
|
|
81
|
+
clientId: 'block-1',
|
|
82
|
+
name: 'core/paragraph',
|
|
83
|
+
innerBlocks: [],
|
|
84
|
+
},
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
const getBlocks =
|
|
88
|
+
overrides.getBlocks ??
|
|
89
|
+
jest.fn().mockReturnValue( overrides.blocks ?? defaultBlocks );
|
|
90
|
+
|
|
91
|
+
( select as jest.Mock ).mockReturnValue( {
|
|
92
|
+
getSelectionStart:
|
|
93
|
+
overrides.getSelectionStart ?? jest.fn().mockReturnValue( {} ),
|
|
94
|
+
getSelectionEnd:
|
|
95
|
+
overrides.getSelectionEnd ?? jest.fn().mockReturnValue( {} ),
|
|
96
|
+
getSelectedBlocksInitialCaretPosition: jest
|
|
97
|
+
.fn()
|
|
98
|
+
.mockReturnValue( null ),
|
|
99
|
+
getBlockIndex: jest.fn().mockReturnValue( 0 ),
|
|
100
|
+
getBlockRootClientId: jest.fn().mockReturnValue( '' ),
|
|
101
|
+
getBlockName: jest
|
|
102
|
+
.fn()
|
|
103
|
+
.mockReturnValue( overrides.getBlockName ?? 'core/paragraph' ),
|
|
104
|
+
getBlocks,
|
|
105
|
+
} );
|
|
57
106
|
|
|
58
|
-
|
|
107
|
+
return { getBlocks };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Helper to create a single Yjs block with optional text content and inner blocks.
|
|
112
|
+
* @param clientId
|
|
113
|
+
* @param name
|
|
114
|
+
* @param options
|
|
115
|
+
* @param options.textContent
|
|
116
|
+
* @param options.innerBlocks
|
|
117
|
+
*/
|
|
118
|
+
function createYBlock(
|
|
119
|
+
clientId: string,
|
|
120
|
+
name: string,
|
|
121
|
+
{
|
|
122
|
+
textContent,
|
|
123
|
+
innerBlocks = [],
|
|
124
|
+
}: { textContent?: string; innerBlocks?: Y.Map< any >[] } = {}
|
|
125
|
+
): Y.Map< any > {
|
|
59
126
|
const block = new Y.Map();
|
|
60
|
-
block.set( 'clientId',
|
|
127
|
+
block.set( 'clientId', clientId );
|
|
128
|
+
block.set( 'name', name );
|
|
129
|
+
|
|
61
130
|
const attrs = new Y.Map();
|
|
62
|
-
|
|
131
|
+
if ( textContent !== undefined ) {
|
|
132
|
+
attrs.set( 'content', new Y.Text( textContent ) );
|
|
133
|
+
}
|
|
134
|
+
|
|
63
135
|
block.set( 'attributes', attrs );
|
|
64
|
-
|
|
65
|
-
|
|
136
|
+
const inner = new Y.Array();
|
|
137
|
+
if ( innerBlocks.length ) {
|
|
138
|
+
inner.push( innerBlocks );
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
block.set( 'innerBlocks', inner );
|
|
142
|
+
return block;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Helper function to create a Y.Doc with blocks structure for testing
|
|
147
|
+
* @param blocks
|
|
148
|
+
*/
|
|
149
|
+
function createTestDocWithBlocks( blocks?: Y.Map< any >[] ) {
|
|
150
|
+
const ydoc = new Y.Doc();
|
|
151
|
+
const documentMap = ydoc.getMap( CRDT_RECORD_MAP_KEY );
|
|
152
|
+
const yBlocks = new Y.Array();
|
|
153
|
+
documentMap.set( 'blocks', yBlocks );
|
|
154
|
+
|
|
155
|
+
if ( blocks ) {
|
|
156
|
+
yBlocks.push( blocks );
|
|
157
|
+
} else {
|
|
158
|
+
// Default: single block with content
|
|
159
|
+
const block = createYBlock( 'block-1', 'core/paragraph', {
|
|
160
|
+
textContent: 'Hello world',
|
|
161
|
+
} );
|
|
162
|
+
yBlocks.push( [ block ] );
|
|
163
|
+
}
|
|
66
164
|
|
|
67
165
|
return ydoc;
|
|
68
166
|
}
|
|
@@ -82,14 +180,7 @@ describe( 'PostEditorAwareness', () => {
|
|
|
82
180
|
|
|
83
181
|
jest.spyOn( Date, 'now' ).mockReturnValue( 1704067200000 );
|
|
84
182
|
|
|
85
|
-
|
|
86
|
-
( select as jest.Mock ).mockReturnValue( {
|
|
87
|
-
getSelectionStart: jest.fn().mockReturnValue( {} ),
|
|
88
|
-
getSelectionEnd: jest.fn().mockReturnValue( {} ),
|
|
89
|
-
getSelectedBlocksInitialCaretPosition: jest
|
|
90
|
-
.fn()
|
|
91
|
-
.mockReturnValue( null ),
|
|
92
|
-
} );
|
|
183
|
+
mockBlockEditorStore();
|
|
93
184
|
|
|
94
185
|
// Mock subscribe to capture the callback
|
|
95
186
|
( subscribe as jest.Mock ).mockImplementation( ( callback ) => {
|
|
@@ -205,12 +296,9 @@ describe( 'PostEditorAwareness', () => {
|
|
|
205
296
|
offset: 5,
|
|
206
297
|
} );
|
|
207
298
|
|
|
208
|
-
(
|
|
299
|
+
mockBlockEditorStore( {
|
|
209
300
|
getSelectionStart: mockGetSelectionStart,
|
|
210
301
|
getSelectionEnd: mockGetSelectionEnd,
|
|
211
|
-
getSelectedBlocksInitialCaretPosition: jest
|
|
212
|
-
.fn()
|
|
213
|
-
.mockReturnValue( null ),
|
|
214
302
|
} );
|
|
215
303
|
|
|
216
304
|
const awareness = new PostEditorAwareness(
|
|
@@ -257,12 +345,9 @@ describe( 'PostEditorAwareness', () => {
|
|
|
257
345
|
offset: 5,
|
|
258
346
|
} );
|
|
259
347
|
|
|
260
|
-
(
|
|
348
|
+
mockBlockEditorStore( {
|
|
261
349
|
getSelectionStart: mockGetSelectionStart,
|
|
262
350
|
getSelectionEnd: mockGetSelectionEnd,
|
|
263
|
-
getSelectedBlocksInitialCaretPosition: jest
|
|
264
|
-
.fn()
|
|
265
|
-
.mockReturnValue( null ),
|
|
266
351
|
} );
|
|
267
352
|
|
|
268
353
|
const awareness = new PostEditorAwareness(
|
|
@@ -339,8 +424,8 @@ describe( 'PostEditorAwareness', () => {
|
|
|
339
424
|
} );
|
|
340
425
|
} );
|
|
341
426
|
|
|
342
|
-
describe( '
|
|
343
|
-
test( 'should return
|
|
427
|
+
describe( 'convertSelectionStateToAbsolute', () => {
|
|
428
|
+
test( 'should return nulls when relative position cannot be resolved', () => {
|
|
344
429
|
const awareness = new PostEditorAwareness(
|
|
345
430
|
doc,
|
|
346
431
|
'postType',
|
|
@@ -361,20 +446,21 @@ describe( 'PostEditorAwareness', () => {
|
|
|
361
446
|
|
|
362
447
|
const selection: SelectionCursor = {
|
|
363
448
|
type: SelectionType.Cursor,
|
|
364
|
-
blockId: 'block-1',
|
|
365
449
|
cursorPosition: {
|
|
366
450
|
relativePosition,
|
|
367
451
|
absoluteOffset: 2,
|
|
368
452
|
},
|
|
369
453
|
};
|
|
370
454
|
|
|
371
|
-
const result =
|
|
455
|
+
const result =
|
|
456
|
+
awareness.convertSelectionStateToAbsolute( selection );
|
|
372
457
|
|
|
373
|
-
// Should return
|
|
374
|
-
expect( result ).toBeNull();
|
|
458
|
+
// Should return nulls when the relative position's type cannot be found
|
|
459
|
+
expect( result.textIndex ).toBeNull();
|
|
460
|
+
expect( result.localClientId ).toBeNull();
|
|
375
461
|
} );
|
|
376
462
|
|
|
377
|
-
test( 'should return
|
|
463
|
+
test( 'should return text index and block client ID for valid cursor selection', () => {
|
|
378
464
|
const awareness = new PostEditorAwareness(
|
|
379
465
|
doc,
|
|
380
466
|
'postType',
|
|
@@ -399,16 +485,49 @@ describe( 'PostEditorAwareness', () => {
|
|
|
399
485
|
|
|
400
486
|
const selection: SelectionCursor = {
|
|
401
487
|
type: SelectionType.Cursor,
|
|
402
|
-
blockId: 'block-1',
|
|
403
488
|
cursorPosition: {
|
|
404
489
|
relativePosition,
|
|
405
490
|
absoluteOffset: 5,
|
|
406
491
|
},
|
|
407
492
|
};
|
|
408
493
|
|
|
409
|
-
const result =
|
|
494
|
+
const result =
|
|
495
|
+
awareness.convertSelectionStateToAbsolute( selection );
|
|
496
|
+
|
|
497
|
+
expect( result.textIndex ).toBe( 5 );
|
|
498
|
+
expect( result.localClientId ).toBe( 'block-1' );
|
|
499
|
+
} );
|
|
500
|
+
|
|
501
|
+
test( 'should resolve WholeBlock selection to block client ID', () => {
|
|
502
|
+
const awareness = new PostEditorAwareness(
|
|
503
|
+
doc,
|
|
504
|
+
'postType',
|
|
505
|
+
'post',
|
|
506
|
+
123
|
|
507
|
+
);
|
|
508
|
+
|
|
509
|
+
// Get the blocks array from the doc
|
|
510
|
+
const documentMap = doc.getMap( CRDT_RECORD_MAP_KEY );
|
|
511
|
+
const blocks = documentMap.get( 'blocks' ) as Y.Array<
|
|
512
|
+
Y.Map< any >
|
|
513
|
+
>;
|
|
514
|
+
|
|
515
|
+
// Create a block relative position
|
|
516
|
+
const blockPosition = Y.createRelativePositionFromTypeIndex(
|
|
517
|
+
blocks,
|
|
518
|
+
0
|
|
519
|
+
);
|
|
520
|
+
|
|
521
|
+
const selection: SelectionWholeBlock = {
|
|
522
|
+
type: SelectionType.WholeBlock,
|
|
523
|
+
blockPosition,
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
const result =
|
|
527
|
+
awareness.convertSelectionStateToAbsolute( selection );
|
|
410
528
|
|
|
411
|
-
expect( result ).
|
|
529
|
+
expect( result.textIndex ).toBeNull();
|
|
530
|
+
expect( result.localClientId ).toBe( 'block-1' );
|
|
412
531
|
} );
|
|
413
532
|
} );
|
|
414
533
|
|
|
@@ -558,4 +677,509 @@ describe( 'PostEditorAwareness', () => {
|
|
|
558
677
|
expect( callback ).toHaveBeenCalled();
|
|
559
678
|
} );
|
|
560
679
|
} );
|
|
680
|
+
|
|
681
|
+
describe( 'convertSelectionStateToAbsolute with nested blocks', () => {
|
|
682
|
+
test( 'should resolve cursor in second root block (path [1])', () => {
|
|
683
|
+
const nestedDoc = createTestDocWithBlocks( [
|
|
684
|
+
createYBlock( 'yjs-block-0', 'core/paragraph', {
|
|
685
|
+
textContent: 'First',
|
|
686
|
+
} ),
|
|
687
|
+
createYBlock( 'yjs-block-1', 'core/paragraph', {
|
|
688
|
+
textContent: 'Second',
|
|
689
|
+
} ),
|
|
690
|
+
createYBlock( 'yjs-block-2', 'core/paragraph', {
|
|
691
|
+
textContent: 'Third',
|
|
692
|
+
} ),
|
|
693
|
+
] );
|
|
694
|
+
|
|
695
|
+
mockBlockEditorStore( {
|
|
696
|
+
blocks: [
|
|
697
|
+
{ clientId: 'local-0', innerBlocks: [] },
|
|
698
|
+
{ clientId: 'local-1', innerBlocks: [] },
|
|
699
|
+
{ clientId: 'local-2', innerBlocks: [] },
|
|
700
|
+
],
|
|
701
|
+
} );
|
|
702
|
+
|
|
703
|
+
const awareness = new PostEditorAwareness(
|
|
704
|
+
nestedDoc,
|
|
705
|
+
'postType',
|
|
706
|
+
'post',
|
|
707
|
+
123
|
|
708
|
+
);
|
|
709
|
+
|
|
710
|
+
// Create a cursor in the third block's text
|
|
711
|
+
const documentMap = nestedDoc.getMap( CRDT_RECORD_MAP_KEY );
|
|
712
|
+
const blocks = documentMap.get( 'blocks' ) as Y.Array<
|
|
713
|
+
Y.Map< any >
|
|
714
|
+
>;
|
|
715
|
+
const block2 = blocks.get( 2 );
|
|
716
|
+
const attrs2 = block2.get( 'attributes' ) as Y.Map< Y.Text >;
|
|
717
|
+
const yText2 = attrs2.get( 'content' ) as Y.Text;
|
|
718
|
+
|
|
719
|
+
const relativePosition = Y.createRelativePositionFromTypeIndex(
|
|
720
|
+
yText2,
|
|
721
|
+
2
|
|
722
|
+
);
|
|
723
|
+
|
|
724
|
+
const selection: SelectionCursor = {
|
|
725
|
+
type: SelectionType.Cursor,
|
|
726
|
+
cursorPosition: {
|
|
727
|
+
relativePosition,
|
|
728
|
+
absoluteOffset: 2,
|
|
729
|
+
},
|
|
730
|
+
};
|
|
731
|
+
|
|
732
|
+
const result =
|
|
733
|
+
awareness.convertSelectionStateToAbsolute( selection );
|
|
734
|
+
|
|
735
|
+
expect( result.textIndex ).toBe( 2 );
|
|
736
|
+
expect( result.localClientId ).toBe( 'local-2' );
|
|
737
|
+
|
|
738
|
+
nestedDoc.destroy();
|
|
739
|
+
} );
|
|
740
|
+
|
|
741
|
+
test( 'should resolve cursor in a nested inner block (path [0, 1])', () => {
|
|
742
|
+
const innerParagraph0 = createYBlock(
|
|
743
|
+
'yjs-inner-0',
|
|
744
|
+
'core/paragraph',
|
|
745
|
+
{ textContent: 'Inner zero' }
|
|
746
|
+
);
|
|
747
|
+
const innerParagraph1 = createYBlock(
|
|
748
|
+
'yjs-inner-1',
|
|
749
|
+
'core/paragraph',
|
|
750
|
+
{ textContent: 'Inner one' }
|
|
751
|
+
);
|
|
752
|
+
const outerColumn = createYBlock( 'yjs-outer', 'core/column', {
|
|
753
|
+
innerBlocks: [ innerParagraph0, innerParagraph1 ],
|
|
754
|
+
} );
|
|
755
|
+
|
|
756
|
+
const nestedDoc = createTestDocWithBlocks( [ outerColumn ] );
|
|
757
|
+
|
|
758
|
+
mockBlockEditorStore( {
|
|
759
|
+
blocks: [
|
|
760
|
+
{
|
|
761
|
+
clientId: 'local-outer',
|
|
762
|
+
innerBlocks: [
|
|
763
|
+
{ clientId: 'local-inner-0', innerBlocks: [] },
|
|
764
|
+
{ clientId: 'local-inner-1', innerBlocks: [] },
|
|
765
|
+
],
|
|
766
|
+
},
|
|
767
|
+
],
|
|
768
|
+
} );
|
|
769
|
+
|
|
770
|
+
const awareness = new PostEditorAwareness(
|
|
771
|
+
nestedDoc,
|
|
772
|
+
'postType',
|
|
773
|
+
'post',
|
|
774
|
+
123
|
|
775
|
+
);
|
|
776
|
+
|
|
777
|
+
// Create cursor in the second inner paragraph
|
|
778
|
+
const documentMap = nestedDoc.getMap( CRDT_RECORD_MAP_KEY );
|
|
779
|
+
const blocks = documentMap.get( 'blocks' ) as Y.Array<
|
|
780
|
+
Y.Map< any >
|
|
781
|
+
>;
|
|
782
|
+
const outer = blocks.get( 0 );
|
|
783
|
+
const innerBlocks = outer.get( 'innerBlocks' ) as Y.Array<
|
|
784
|
+
Y.Map< any >
|
|
785
|
+
>;
|
|
786
|
+
const innerBlock1 = innerBlocks.get( 1 );
|
|
787
|
+
const innerAttrs = innerBlock1.get(
|
|
788
|
+
'attributes'
|
|
789
|
+
) as Y.Map< Y.Text >;
|
|
790
|
+
const yText = innerAttrs.get( 'content' ) as Y.Text;
|
|
791
|
+
|
|
792
|
+
const relativePosition = Y.createRelativePositionFromTypeIndex(
|
|
793
|
+
yText,
|
|
794
|
+
5
|
|
795
|
+
);
|
|
796
|
+
|
|
797
|
+
const selection: SelectionCursor = {
|
|
798
|
+
type: SelectionType.Cursor,
|
|
799
|
+
cursorPosition: {
|
|
800
|
+
relativePosition,
|
|
801
|
+
absoluteOffset: 5,
|
|
802
|
+
},
|
|
803
|
+
};
|
|
804
|
+
|
|
805
|
+
const result =
|
|
806
|
+
awareness.convertSelectionStateToAbsolute( selection );
|
|
807
|
+
|
|
808
|
+
expect( result.textIndex ).toBe( 5 );
|
|
809
|
+
expect( result.localClientId ).toBe( 'local-inner-1' );
|
|
810
|
+
|
|
811
|
+
nestedDoc.destroy();
|
|
812
|
+
} );
|
|
813
|
+
|
|
814
|
+
test( 'should resolve WholeBlock for a nested image block', () => {
|
|
815
|
+
const innerImage = createYBlock( 'yjs-img', 'core/image' );
|
|
816
|
+
const outerColumn = createYBlock( 'yjs-col', 'core/column', {
|
|
817
|
+
innerBlocks: [ innerImage ],
|
|
818
|
+
} );
|
|
819
|
+
|
|
820
|
+
const nestedDoc = createTestDocWithBlocks( [ outerColumn ] );
|
|
821
|
+
|
|
822
|
+
mockBlockEditorStore( {
|
|
823
|
+
blocks: [
|
|
824
|
+
{
|
|
825
|
+
clientId: 'local-col',
|
|
826
|
+
innerBlocks: [
|
|
827
|
+
{ clientId: 'local-img', innerBlocks: [] },
|
|
828
|
+
],
|
|
829
|
+
},
|
|
830
|
+
],
|
|
831
|
+
} );
|
|
832
|
+
|
|
833
|
+
const awareness = new PostEditorAwareness(
|
|
834
|
+
nestedDoc,
|
|
835
|
+
'postType',
|
|
836
|
+
'post',
|
|
837
|
+
123
|
|
838
|
+
);
|
|
839
|
+
|
|
840
|
+
// Create a WholeBlock relative position for the inner image
|
|
841
|
+
const documentMap = nestedDoc.getMap( CRDT_RECORD_MAP_KEY );
|
|
842
|
+
const blocks = documentMap.get( 'blocks' ) as Y.Array<
|
|
843
|
+
Y.Map< any >
|
|
844
|
+
>;
|
|
845
|
+
const outer = blocks.get( 0 );
|
|
846
|
+
const innerBlocks = outer.get( 'innerBlocks' ) as Y.Array<
|
|
847
|
+
Y.Map< any >
|
|
848
|
+
>;
|
|
849
|
+
|
|
850
|
+
const blockPosition = Y.createRelativePositionFromTypeIndex(
|
|
851
|
+
innerBlocks,
|
|
852
|
+
0
|
|
853
|
+
);
|
|
854
|
+
|
|
855
|
+
const selection: SelectionWholeBlock = {
|
|
856
|
+
type: SelectionType.WholeBlock,
|
|
857
|
+
blockPosition,
|
|
858
|
+
};
|
|
859
|
+
|
|
860
|
+
const result =
|
|
861
|
+
awareness.convertSelectionStateToAbsolute( selection );
|
|
862
|
+
|
|
863
|
+
expect( result.textIndex ).toBeNull();
|
|
864
|
+
expect( result.localClientId ).toBe( 'local-img' );
|
|
865
|
+
|
|
866
|
+
nestedDoc.destroy();
|
|
867
|
+
} );
|
|
868
|
+
|
|
869
|
+
test( 'should resolve a deeply nested block (path [1, 0, 1])', () => {
|
|
870
|
+
const deepParagraph0 = createYBlock(
|
|
871
|
+
'yjs-deep-0',
|
|
872
|
+
'core/paragraph',
|
|
873
|
+
{ textContent: 'Deep zero' }
|
|
874
|
+
);
|
|
875
|
+
const deepParagraph1 = createYBlock(
|
|
876
|
+
'yjs-deep-1',
|
|
877
|
+
'core/paragraph',
|
|
878
|
+
{ textContent: 'Deep one content' }
|
|
879
|
+
);
|
|
880
|
+
const midColumn = createYBlock( 'yjs-mid', 'core/column', {
|
|
881
|
+
innerBlocks: [ deepParagraph0, deepParagraph1 ],
|
|
882
|
+
} );
|
|
883
|
+
const outerColumns0 = createYBlock( 'yjs-outer-0', 'core/columns' );
|
|
884
|
+
const outerColumns1 = createYBlock( 'yjs-outer-1', 'core/columns', {
|
|
885
|
+
innerBlocks: [ midColumn ],
|
|
886
|
+
} );
|
|
887
|
+
|
|
888
|
+
const nestedDoc = createTestDocWithBlocks( [
|
|
889
|
+
outerColumns0,
|
|
890
|
+
outerColumns1,
|
|
891
|
+
] );
|
|
892
|
+
|
|
893
|
+
mockBlockEditorStore( {
|
|
894
|
+
blocks: [
|
|
895
|
+
{ clientId: 'local-outer-0', innerBlocks: [] },
|
|
896
|
+
{
|
|
897
|
+
clientId: 'local-outer-1',
|
|
898
|
+
innerBlocks: [
|
|
899
|
+
{
|
|
900
|
+
clientId: 'local-mid',
|
|
901
|
+
innerBlocks: [
|
|
902
|
+
{
|
|
903
|
+
clientId: 'local-deep-0',
|
|
904
|
+
innerBlocks: [],
|
|
905
|
+
},
|
|
906
|
+
{
|
|
907
|
+
clientId: 'local-deep-1',
|
|
908
|
+
innerBlocks: [],
|
|
909
|
+
},
|
|
910
|
+
],
|
|
911
|
+
},
|
|
912
|
+
],
|
|
913
|
+
},
|
|
914
|
+
],
|
|
915
|
+
} );
|
|
916
|
+
|
|
917
|
+
const awareness = new PostEditorAwareness(
|
|
918
|
+
nestedDoc,
|
|
919
|
+
'postType',
|
|
920
|
+
'post',
|
|
921
|
+
123
|
|
922
|
+
);
|
|
923
|
+
|
|
924
|
+
// Create cursor in the deeply nested second paragraph
|
|
925
|
+
const documentMap = nestedDoc.getMap( CRDT_RECORD_MAP_KEY );
|
|
926
|
+
const blocks = documentMap.get( 'blocks' ) as Y.Array<
|
|
927
|
+
Y.Map< any >
|
|
928
|
+
>;
|
|
929
|
+
const outer1 = blocks.get( 1 );
|
|
930
|
+
const outer1Inner = outer1.get( 'innerBlocks' ) as Y.Array<
|
|
931
|
+
Y.Map< any >
|
|
932
|
+
>;
|
|
933
|
+
const mid = outer1Inner.get( 0 );
|
|
934
|
+
const midInner = mid.get( 'innerBlocks' ) as Y.Array<
|
|
935
|
+
Y.Map< any >
|
|
936
|
+
>;
|
|
937
|
+
const deep1 = midInner.get( 1 );
|
|
938
|
+
const deep1Attrs = deep1.get( 'attributes' ) as Y.Map< Y.Text >;
|
|
939
|
+
const yText = deep1Attrs.get( 'content' ) as Y.Text;
|
|
940
|
+
|
|
941
|
+
const relativePosition = Y.createRelativePositionFromTypeIndex(
|
|
942
|
+
yText,
|
|
943
|
+
7
|
|
944
|
+
);
|
|
945
|
+
|
|
946
|
+
const selection: SelectionCursor = {
|
|
947
|
+
type: SelectionType.Cursor,
|
|
948
|
+
cursorPosition: {
|
|
949
|
+
relativePosition,
|
|
950
|
+
absoluteOffset: 7,
|
|
951
|
+
},
|
|
952
|
+
};
|
|
953
|
+
|
|
954
|
+
const result =
|
|
955
|
+
awareness.convertSelectionStateToAbsolute( selection );
|
|
956
|
+
|
|
957
|
+
expect( result.textIndex ).toBe( 7 );
|
|
958
|
+
expect( result.localClientId ).toBe( 'local-deep-1' );
|
|
959
|
+
|
|
960
|
+
nestedDoc.destroy();
|
|
961
|
+
} );
|
|
962
|
+
} );
|
|
963
|
+
|
|
964
|
+
describe( 'template mode (core/post-content handling)', () => {
|
|
965
|
+
test( 'should resolve cursor when getBlocks returns template tree with core/post-content', () => {
|
|
966
|
+
// Yjs doc has only the post content blocks (no template wrapper)
|
|
967
|
+
const templateDoc = createTestDocWithBlocks( [
|
|
968
|
+
createYBlock( 'yjs-para-0', 'core/paragraph', {
|
|
969
|
+
textContent: 'Post paragraph 1',
|
|
970
|
+
} ),
|
|
971
|
+
createYBlock( 'yjs-para-1', 'core/paragraph', {
|
|
972
|
+
textContent: 'Post paragraph 2',
|
|
973
|
+
} ),
|
|
974
|
+
] );
|
|
975
|
+
|
|
976
|
+
// In template mode, getBlocks() returns the full template tree.
|
|
977
|
+
// The Yjs paths are relative to post content, so the receiver needs
|
|
978
|
+
// to find core/post-content and navigate from there.
|
|
979
|
+
const postContentClientId = 'local-post-content';
|
|
980
|
+
const mockGetBlocks = jest
|
|
981
|
+
.fn()
|
|
982
|
+
.mockImplementation( ( rootClientId?: string ) => {
|
|
983
|
+
if ( rootClientId === postContentClientId ) {
|
|
984
|
+
// Controlled inner blocks of core/post-content
|
|
985
|
+
return [
|
|
986
|
+
{
|
|
987
|
+
clientId: 'local-para-0',
|
|
988
|
+
name: 'core/paragraph',
|
|
989
|
+
innerBlocks: [],
|
|
990
|
+
},
|
|
991
|
+
{
|
|
992
|
+
clientId: 'local-para-1',
|
|
993
|
+
name: 'core/paragraph',
|
|
994
|
+
innerBlocks: [],
|
|
995
|
+
},
|
|
996
|
+
];
|
|
997
|
+
}
|
|
998
|
+
// Full template tree
|
|
999
|
+
return [
|
|
1000
|
+
{
|
|
1001
|
+
clientId: 'local-header',
|
|
1002
|
+
name: 'core/template-part',
|
|
1003
|
+
innerBlocks: [],
|
|
1004
|
+
},
|
|
1005
|
+
{
|
|
1006
|
+
clientId: 'local-group',
|
|
1007
|
+
name: 'core/group',
|
|
1008
|
+
innerBlocks: [
|
|
1009
|
+
{
|
|
1010
|
+
clientId: postContentClientId,
|
|
1011
|
+
name: 'core/post-content',
|
|
1012
|
+
innerBlocks: [], // empty because they're controlled inner blocks
|
|
1013
|
+
},
|
|
1014
|
+
],
|
|
1015
|
+
},
|
|
1016
|
+
{
|
|
1017
|
+
clientId: 'local-footer',
|
|
1018
|
+
name: 'core/template-part',
|
|
1019
|
+
innerBlocks: [],
|
|
1020
|
+
},
|
|
1021
|
+
];
|
|
1022
|
+
} );
|
|
1023
|
+
|
|
1024
|
+
mockBlockEditorStore( { getBlocks: mockGetBlocks } );
|
|
1025
|
+
|
|
1026
|
+
const awareness = new PostEditorAwareness(
|
|
1027
|
+
templateDoc,
|
|
1028
|
+
'postType',
|
|
1029
|
+
'post',
|
|
1030
|
+
123
|
|
1031
|
+
);
|
|
1032
|
+
|
|
1033
|
+
// Create cursor in the second post content paragraph
|
|
1034
|
+
const documentMap = templateDoc.getMap( CRDT_RECORD_MAP_KEY );
|
|
1035
|
+
const blocks = documentMap.get( 'blocks' ) as Y.Array<
|
|
1036
|
+
Y.Map< any >
|
|
1037
|
+
>;
|
|
1038
|
+
const block1 = blocks.get( 1 );
|
|
1039
|
+
const attrs = block1.get( 'attributes' ) as Y.Map< Y.Text >;
|
|
1040
|
+
const yText = attrs.get( 'content' ) as Y.Text;
|
|
1041
|
+
|
|
1042
|
+
const relativePosition = Y.createRelativePositionFromTypeIndex(
|
|
1043
|
+
yText,
|
|
1044
|
+
4
|
|
1045
|
+
);
|
|
1046
|
+
|
|
1047
|
+
const selection: SelectionCursor = {
|
|
1048
|
+
type: SelectionType.Cursor,
|
|
1049
|
+
cursorPosition: {
|
|
1050
|
+
relativePosition,
|
|
1051
|
+
absoluteOffset: 4,
|
|
1052
|
+
},
|
|
1053
|
+
};
|
|
1054
|
+
|
|
1055
|
+
const result =
|
|
1056
|
+
awareness.convertSelectionStateToAbsolute( selection );
|
|
1057
|
+
|
|
1058
|
+
expect( result.textIndex ).toBe( 4 );
|
|
1059
|
+
// Should resolve to the post-content inner block, not a template block
|
|
1060
|
+
expect( result.localClientId ).toBe( 'local-para-1' );
|
|
1061
|
+
// Verify getBlocks was called with the post-content clientId
|
|
1062
|
+
expect( mockGetBlocks ).toHaveBeenCalledWith( postContentClientId );
|
|
1063
|
+
|
|
1064
|
+
templateDoc.destroy();
|
|
1065
|
+
} );
|
|
1066
|
+
|
|
1067
|
+
test( 'should resolve WholeBlock in template mode', () => {
|
|
1068
|
+
const templateDoc = createTestDocWithBlocks( [
|
|
1069
|
+
createYBlock( 'yjs-img', 'core/image' ),
|
|
1070
|
+
] );
|
|
1071
|
+
|
|
1072
|
+
const postContentClientId = 'local-post-content';
|
|
1073
|
+
const mockGetBlocks = jest
|
|
1074
|
+
.fn()
|
|
1075
|
+
.mockImplementation( ( rootClientId?: string ) => {
|
|
1076
|
+
if ( rootClientId === postContentClientId ) {
|
|
1077
|
+
return [
|
|
1078
|
+
{
|
|
1079
|
+
clientId: 'local-img',
|
|
1080
|
+
name: 'core/image',
|
|
1081
|
+
innerBlocks: [],
|
|
1082
|
+
},
|
|
1083
|
+
];
|
|
1084
|
+
}
|
|
1085
|
+
return [
|
|
1086
|
+
{
|
|
1087
|
+
clientId: 'local-group',
|
|
1088
|
+
name: 'core/group',
|
|
1089
|
+
innerBlocks: [
|
|
1090
|
+
{
|
|
1091
|
+
clientId: postContentClientId,
|
|
1092
|
+
name: 'core/post-content',
|
|
1093
|
+
innerBlocks: [],
|
|
1094
|
+
},
|
|
1095
|
+
],
|
|
1096
|
+
},
|
|
1097
|
+
];
|
|
1098
|
+
} );
|
|
1099
|
+
|
|
1100
|
+
mockBlockEditorStore( {
|
|
1101
|
+
getBlocks: mockGetBlocks,
|
|
1102
|
+
getBlockName: 'core/image',
|
|
1103
|
+
} );
|
|
1104
|
+
|
|
1105
|
+
const awareness = new PostEditorAwareness(
|
|
1106
|
+
templateDoc,
|
|
1107
|
+
'postType',
|
|
1108
|
+
'post',
|
|
1109
|
+
123
|
|
1110
|
+
);
|
|
1111
|
+
|
|
1112
|
+
const documentMap = templateDoc.getMap( CRDT_RECORD_MAP_KEY );
|
|
1113
|
+
const blocks = documentMap.get( 'blocks' ) as Y.Array<
|
|
1114
|
+
Y.Map< any >
|
|
1115
|
+
>;
|
|
1116
|
+
|
|
1117
|
+
const blockPosition = Y.createRelativePositionFromTypeIndex(
|
|
1118
|
+
blocks,
|
|
1119
|
+
0
|
|
1120
|
+
);
|
|
1121
|
+
|
|
1122
|
+
const selection: SelectionWholeBlock = {
|
|
1123
|
+
type: SelectionType.WholeBlock,
|
|
1124
|
+
blockPosition,
|
|
1125
|
+
};
|
|
1126
|
+
|
|
1127
|
+
const result =
|
|
1128
|
+
awareness.convertSelectionStateToAbsolute( selection );
|
|
1129
|
+
|
|
1130
|
+
expect( result.textIndex ).toBeNull();
|
|
1131
|
+
expect( result.localClientId ).toBe( 'local-img' );
|
|
1132
|
+
|
|
1133
|
+
templateDoc.destroy();
|
|
1134
|
+
} );
|
|
1135
|
+
|
|
1136
|
+
test( 'should fall back to root blocks when no core/post-content exists', () => {
|
|
1137
|
+
// Normal mode (no template) — should use root blocks directly
|
|
1138
|
+
const normalDoc = createTestDocWithBlocks( [
|
|
1139
|
+
createYBlock( 'yjs-para', 'core/paragraph', {
|
|
1140
|
+
textContent: 'Normal mode',
|
|
1141
|
+
} ),
|
|
1142
|
+
] );
|
|
1143
|
+
|
|
1144
|
+
mockBlockEditorStore( {
|
|
1145
|
+
blocks: [ { clientId: 'local-para', innerBlocks: [] } ],
|
|
1146
|
+
} );
|
|
1147
|
+
|
|
1148
|
+
const awareness = new PostEditorAwareness(
|
|
1149
|
+
normalDoc,
|
|
1150
|
+
'postType',
|
|
1151
|
+
'post',
|
|
1152
|
+
123
|
|
1153
|
+
);
|
|
1154
|
+
|
|
1155
|
+
const documentMap = normalDoc.getMap( CRDT_RECORD_MAP_KEY );
|
|
1156
|
+
const blocks = documentMap.get( 'blocks' ) as Y.Array<
|
|
1157
|
+
Y.Map< any >
|
|
1158
|
+
>;
|
|
1159
|
+
const block = blocks.get( 0 );
|
|
1160
|
+
const attrs = block.get( 'attributes' ) as Y.Map< Y.Text >;
|
|
1161
|
+
const yText = attrs.get( 'content' ) as Y.Text;
|
|
1162
|
+
|
|
1163
|
+
const relativePosition = Y.createRelativePositionFromTypeIndex(
|
|
1164
|
+
yText,
|
|
1165
|
+
3
|
|
1166
|
+
);
|
|
1167
|
+
|
|
1168
|
+
const selection: SelectionCursor = {
|
|
1169
|
+
type: SelectionType.Cursor,
|
|
1170
|
+
cursorPosition: {
|
|
1171
|
+
relativePosition,
|
|
1172
|
+
absoluteOffset: 3,
|
|
1173
|
+
},
|
|
1174
|
+
};
|
|
1175
|
+
|
|
1176
|
+
const result =
|
|
1177
|
+
awareness.convertSelectionStateToAbsolute( selection );
|
|
1178
|
+
|
|
1179
|
+
expect( result.textIndex ).toBe( 3 );
|
|
1180
|
+
expect( result.localClientId ).toBe( 'local-para' );
|
|
1181
|
+
|
|
1182
|
+
normalDoc.destroy();
|
|
1183
|
+
} );
|
|
1184
|
+
} );
|
|
561
1185
|
} );
|