@wordpress/core-data 7.40.1 → 7.40.2-next.v.202602271551.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 +63 -59
- 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 +116 -125
- package/build/resolvers.cjs.map +2 -2
- package/build/selectors.cjs.map +2 -2
- package/build/sync.cjs +1 -7
- package/build/sync.cjs.map +2 -2
- 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 +47 -19
- 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/utils/crdt.cjs +12 -1
- package/build/utils/crdt.cjs.map +2 -2
- 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 +65 -60
- 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 +116 -124
- package/build-module/resolvers.mjs.map +2 -2
- package/build-module/selectors.mjs.map +2 -2
- package/build-module/sync.mjs +1 -5
- package/build-module/sync.mjs.map +2 -2
- 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 +46 -19
- 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-module/utils/crdt.mjs +16 -6
- package/build-module/utils/crdt.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 +14 -5
- 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-user-selections.d.ts +21 -4
- package/build-types/utils/crdt-user-selections.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/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 +71 -62
- 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 +158 -171
- package/src/selectors.ts +0 -7
- package/src/sync.ts +0 -4
- package/src/test/entities.js +39 -10
- package/src/test/resolvers.js +155 -81
- package/src/types.ts +23 -5
- package/src/utils/crdt-blocks.ts +113 -47
- package/src/utils/crdt-selection.ts +87 -25
- package/src/utils/crdt-user-selections.ts +129 -47
- package/src/utils/crdt.ts +23 -7
- package/src/utils/test/crdt-blocks.ts +591 -0
- package/src/utils/test/crdt-user-selections.ts +894 -0
- package/src/utils/test/crdt.ts +136 -10
|
@@ -10,6 +10,7 @@ import { store as blockEditorStore } from '@wordpress/block-editor';
|
|
|
10
10
|
* Internal dependencies
|
|
11
11
|
*/
|
|
12
12
|
import { BaseAwarenessState, baseEqualityFieldChecks } from './base-awareness';
|
|
13
|
+
import { getBlockPathInYdoc, resolveBlockClientIdByPath } from './block-lookup';
|
|
13
14
|
import {
|
|
14
15
|
AWARENESS_CURSOR_UPDATE_THROTTLE_IN_MS,
|
|
15
16
|
LOCAL_CURSOR_UPDATE_DEBOUNCE_IN_MS,
|
|
@@ -18,9 +19,11 @@ import { STORE_NAME as coreStore } from '../name';
|
|
|
18
19
|
import {
|
|
19
20
|
areSelectionsStatesEqual,
|
|
20
21
|
getSelectionState,
|
|
22
|
+
SelectionType,
|
|
21
23
|
} from '../utils/crdt-user-selections';
|
|
22
24
|
|
|
23
|
-
import type {
|
|
25
|
+
import type { SelectionState, WPBlockSelection } from '../types';
|
|
26
|
+
import type { YBlocks } from '../utils/crdt-blocks';
|
|
24
27
|
import type {
|
|
25
28
|
DebugCollaboratorData,
|
|
26
29
|
EditorState,
|
|
@@ -172,20 +175,74 @@ export class PostEditorAwareness extends BaseAwarenessState< PostEditorState > {
|
|
|
172
175
|
}
|
|
173
176
|
|
|
174
177
|
/**
|
|
175
|
-
*
|
|
178
|
+
* Resolve a selection state to a text index and block client ID.
|
|
176
179
|
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
180
|
+
* For text-based selections, navigates up from the resolved Y.Text via
|
|
181
|
+
* AbstractType.parent to find the containing block, then resolves the
|
|
182
|
+
* local clientId via the block's tree path.
|
|
183
|
+
* For WholeBlock selections, resolves the block's relative position and
|
|
184
|
+
* then finds the local clientId via tree path.
|
|
185
|
+
*
|
|
186
|
+
* Tree-path resolution is used instead of reading the clientId directly
|
|
187
|
+
* from the Yjs block because the local block-editor store may use different
|
|
188
|
+
* clientIds (e.g. in "Show Template" mode where blocks are cloned).
|
|
189
|
+
*
|
|
190
|
+
* @param selection - The selection state.
|
|
191
|
+
* @return The text index and block client ID, or nulls if not resolvable.
|
|
179
192
|
*/
|
|
180
|
-
public
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
193
|
+
public convertSelectionStateToAbsolute( selection: SelectionState ): {
|
|
194
|
+
textIndex: number | null;
|
|
195
|
+
localClientId: string | null;
|
|
196
|
+
} {
|
|
197
|
+
if ( selection.type === SelectionType.None ) {
|
|
198
|
+
return { textIndex: null, localClientId: null };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if ( selection.type === SelectionType.WholeBlock ) {
|
|
202
|
+
const absolutePos = Y.createAbsolutePositionFromRelativePosition(
|
|
203
|
+
selection.blockPosition,
|
|
186
204
|
this.doc
|
|
187
|
-
)
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
let localClientId: string | null = null;
|
|
208
|
+
|
|
209
|
+
if ( absolutePos && absolutePos.type instanceof Y.Array ) {
|
|
210
|
+
const parentArray = absolutePos.type as YBlocks;
|
|
211
|
+
const block = parentArray.get( absolutePos.index );
|
|
212
|
+
|
|
213
|
+
if ( block instanceof Y.Map ) {
|
|
214
|
+
const path = getBlockPathInYdoc( block );
|
|
215
|
+
localClientId = path
|
|
216
|
+
? resolveBlockClientIdByPath( path )
|
|
217
|
+
: null;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return { textIndex: null, localClientId };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Text-based selections: resolve cursor position and navigate up.
|
|
225
|
+
const cursorPos =
|
|
226
|
+
'cursorPosition' in selection
|
|
227
|
+
? selection.cursorPosition
|
|
228
|
+
: selection.cursorStartPosition;
|
|
229
|
+
|
|
230
|
+
const absolutePosition = Y.createAbsolutePositionFromRelativePosition(
|
|
231
|
+
cursorPos.relativePosition,
|
|
232
|
+
this.doc
|
|
188
233
|
);
|
|
234
|
+
|
|
235
|
+
if ( ! absolutePosition ) {
|
|
236
|
+
return { textIndex: null, localClientId: null };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Navigate up: Y.Text -> attributes Y.Map -> block Y.Map
|
|
240
|
+
const yType = absolutePosition.type.parent?.parent;
|
|
241
|
+
const path =
|
|
242
|
+
yType instanceof Y.Map ? getBlockPathInYdoc( yType ) : null;
|
|
243
|
+
const localClientId = path ? resolveBlockClientIdByPath( path ) : null;
|
|
244
|
+
|
|
245
|
+
return { textIndex: absolutePosition.index, localClientId };
|
|
189
246
|
}
|
|
190
247
|
|
|
191
248
|
/**
|
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External dependencies
|
|
3
|
+
*/
|
|
4
|
+
import { Y } from '@wordpress/sync';
|
|
5
|
+
import { select } from '@wordpress/data';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Internal dependencies
|
|
9
|
+
*/
|
|
10
|
+
import {
|
|
11
|
+
getBlockPathInYdoc,
|
|
12
|
+
resolveBlockClientIdByPath,
|
|
13
|
+
} from '../block-lookup';
|
|
14
|
+
|
|
15
|
+
// Mock WordPress dependencies
|
|
16
|
+
jest.mock( '@wordpress/data', () => ( {
|
|
17
|
+
select: jest.fn(),
|
|
18
|
+
} ) );
|
|
19
|
+
|
|
20
|
+
jest.mock( '@wordpress/block-editor', () => ( {
|
|
21
|
+
store: 'core/block-editor',
|
|
22
|
+
} ) );
|
|
23
|
+
|
|
24
|
+
type MockBlock = {
|
|
25
|
+
clientId: string;
|
|
26
|
+
name: string;
|
|
27
|
+
innerBlocks: MockBlock[];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Create a Y.Map block with a clientId and empty innerBlocks, matching the
|
|
32
|
+
* shape used by the Yjs block tree.
|
|
33
|
+
*
|
|
34
|
+
* @param clientId Block client ID.
|
|
35
|
+
*/
|
|
36
|
+
function createTestYBlock( clientId: string ): Y.Map< any > {
|
|
37
|
+
const block = new Y.Map< any >();
|
|
38
|
+
block.set( 'clientId', clientId );
|
|
39
|
+
block.set( 'innerBlocks', new Y.Array< Y.Map< any > >() );
|
|
40
|
+
return block;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Create a Y.Map block with a clientId and the given innerBlocks array,
|
|
45
|
+
* for blocks that need pre-populated children.
|
|
46
|
+
*
|
|
47
|
+
* @param clientId Block client ID.
|
|
48
|
+
* @param innerBlocks A Y.Array to use as the block's innerBlocks.
|
|
49
|
+
*/
|
|
50
|
+
function createTestYBlockWithInner(
|
|
51
|
+
clientId: string,
|
|
52
|
+
innerBlocks: Y.Array< Y.Map< any > >
|
|
53
|
+
): Y.Map< any > {
|
|
54
|
+
const block = new Y.Map< any >();
|
|
55
|
+
block.set( 'clientId', clientId );
|
|
56
|
+
block.set( 'innerBlocks', innerBlocks );
|
|
57
|
+
return block;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Helper to create a Y.Doc with a flat list of blocks, each having a
|
|
62
|
+
* `clientId` and an empty `innerBlocks` Y.Array.
|
|
63
|
+
*
|
|
64
|
+
* @param count Number of root blocks to create.
|
|
65
|
+
* @return The Y.Doc and the root blocks Y.Array.
|
|
66
|
+
*/
|
|
67
|
+
function createFlatYDoc( count: number ) {
|
|
68
|
+
const ydoc = new Y.Doc();
|
|
69
|
+
const rootMap = ydoc.getMap( 'test' );
|
|
70
|
+
const blocks = new Y.Array< Y.Map< any > >();
|
|
71
|
+
rootMap.set( 'blocks', blocks );
|
|
72
|
+
|
|
73
|
+
const yBlocks: Y.Map< any >[] = [];
|
|
74
|
+
for ( let i = 0; i < count; i++ ) {
|
|
75
|
+
yBlocks.push( createTestYBlock( `block-${ i }` ) );
|
|
76
|
+
}
|
|
77
|
+
blocks.push( yBlocks );
|
|
78
|
+
|
|
79
|
+
return { ydoc, blocks };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Helper to create a Y.Doc with nested blocks.
|
|
84
|
+
*
|
|
85
|
+
* Creates `rootCount` root blocks, then adds `innerCount` inner blocks
|
|
86
|
+
* to the block at index `parentIndex`.
|
|
87
|
+
* @param root0
|
|
88
|
+
* @param root0.rootCount
|
|
89
|
+
* @param root0.parentIndex
|
|
90
|
+
* @param root0.innerCount
|
|
91
|
+
*/
|
|
92
|
+
function createNestedYDoc( {
|
|
93
|
+
rootCount,
|
|
94
|
+
parentIndex,
|
|
95
|
+
innerCount,
|
|
96
|
+
}: {
|
|
97
|
+
rootCount: number;
|
|
98
|
+
parentIndex: number;
|
|
99
|
+
innerCount: number;
|
|
100
|
+
} ) {
|
|
101
|
+
const ydoc = new Y.Doc();
|
|
102
|
+
const rootMap = ydoc.getMap( 'test' );
|
|
103
|
+
const rootBlocks = new Y.Array< Y.Map< any > >();
|
|
104
|
+
rootMap.set( 'blocks', rootBlocks );
|
|
105
|
+
|
|
106
|
+
const yRootBlocks: Y.Map< any >[] = [];
|
|
107
|
+
for ( let i = 0; i < rootCount; i++ ) {
|
|
108
|
+
yRootBlocks.push( createTestYBlock( `root-${ i }` ) );
|
|
109
|
+
}
|
|
110
|
+
rootBlocks.push( yRootBlocks );
|
|
111
|
+
|
|
112
|
+
// Add inner blocks to the specified parent.
|
|
113
|
+
const parentBlock = rootBlocks.get( parentIndex );
|
|
114
|
+
const innerBlocksArray = parentBlock.get( 'innerBlocks' ) as Y.Array<
|
|
115
|
+
Y.Map< any >
|
|
116
|
+
>;
|
|
117
|
+
|
|
118
|
+
const yInnerBlocks: Y.Map< any >[] = [];
|
|
119
|
+
for ( let j = 0; j < innerCount; j++ ) {
|
|
120
|
+
yInnerBlocks.push(
|
|
121
|
+
createTestYBlock( `inner-${ parentIndex }-${ j }` )
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
innerBlocksArray.push( yInnerBlocks );
|
|
125
|
+
|
|
126
|
+
return { ydoc, rootBlocks, innerBlocksArray };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Mock the block-editor store's `getBlocks` selector.
|
|
131
|
+
*
|
|
132
|
+
* When called without an argument (or undefined), returns `rootBlocks`.
|
|
133
|
+
* When called with a clientId, looks up the block by clientId and returns
|
|
134
|
+
* its innerBlocks. This mimics how `getBlocks( clientId )` works in the
|
|
135
|
+
* real store for controlled inner blocks.
|
|
136
|
+
* @param rootBlocks
|
|
137
|
+
*/
|
|
138
|
+
function mockBlockEditorStore( rootBlocks: MockBlock[] ) {
|
|
139
|
+
const allBlocks = new Map< string, MockBlock >();
|
|
140
|
+
|
|
141
|
+
function indexBlocks( blocks: MockBlock[] ) {
|
|
142
|
+
for ( const block of blocks ) {
|
|
143
|
+
allBlocks.set( block.clientId, block );
|
|
144
|
+
if ( block.innerBlocks?.length ) {
|
|
145
|
+
indexBlocks( block.innerBlocks );
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
indexBlocks( rootBlocks );
|
|
150
|
+
|
|
151
|
+
const getBlocks = jest.fn( ( rootClientId?: string ) => {
|
|
152
|
+
if ( rootClientId === undefined ) {
|
|
153
|
+
return rootBlocks;
|
|
154
|
+
}
|
|
155
|
+
const block = allBlocks.get( rootClientId );
|
|
156
|
+
return block ? block.innerBlocks : [];
|
|
157
|
+
} );
|
|
158
|
+
|
|
159
|
+
( select as jest.Mock ).mockReturnValue( { getBlocks } );
|
|
160
|
+
return { getBlocks };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
describe( 'getBlockPathInYdoc', () => {
|
|
164
|
+
it( 'should return path [0] for the first root block', () => {
|
|
165
|
+
const { blocks } = createFlatYDoc( 3 );
|
|
166
|
+
const firstBlock = blocks.get( 0 );
|
|
167
|
+
|
|
168
|
+
expect( getBlockPathInYdoc( firstBlock ) ).toEqual( [ 0 ] );
|
|
169
|
+
} );
|
|
170
|
+
|
|
171
|
+
it( 'should return path [2] for the third root block', () => {
|
|
172
|
+
const { blocks } = createFlatYDoc( 3 );
|
|
173
|
+
const thirdBlock = blocks.get( 2 );
|
|
174
|
+
|
|
175
|
+
expect( getBlockPathInYdoc( thirdBlock ) ).toEqual( [ 2 ] );
|
|
176
|
+
} );
|
|
177
|
+
|
|
178
|
+
it( 'should return a nested path for an inner block', () => {
|
|
179
|
+
const { innerBlocksArray } = createNestedYDoc( {
|
|
180
|
+
rootCount: 2,
|
|
181
|
+
parentIndex: 1,
|
|
182
|
+
innerCount: 3,
|
|
183
|
+
} );
|
|
184
|
+
|
|
185
|
+
// Second inner block of the second root block → [1, 1]
|
|
186
|
+
const innerBlock = innerBlocksArray.get( 1 );
|
|
187
|
+
expect( getBlockPathInYdoc( innerBlock ) ).toEqual( [ 1, 1 ] );
|
|
188
|
+
} );
|
|
189
|
+
|
|
190
|
+
it( 'should return [parentIndex, 0] for the first inner block', () => {
|
|
191
|
+
const { innerBlocksArray } = createNestedYDoc( {
|
|
192
|
+
rootCount: 3,
|
|
193
|
+
parentIndex: 0,
|
|
194
|
+
innerCount: 2,
|
|
195
|
+
} );
|
|
196
|
+
|
|
197
|
+
const firstInner = innerBlocksArray.get( 0 );
|
|
198
|
+
expect( getBlockPathInYdoc( firstInner ) ).toEqual( [ 0, 0 ] );
|
|
199
|
+
} );
|
|
200
|
+
|
|
201
|
+
it( 'should return null for a Y.Map without a parent array', () => {
|
|
202
|
+
const orphan = new Y.Map< any >();
|
|
203
|
+
orphan.set( 'clientId', 'orphan' );
|
|
204
|
+
|
|
205
|
+
expect( getBlockPathInYdoc( orphan ) ).toBeNull();
|
|
206
|
+
} );
|
|
207
|
+
|
|
208
|
+
it( 'should handle deeply nested blocks', () => {
|
|
209
|
+
// Build a 3-level deep structure so the target block is at [2, 7, 1].
|
|
210
|
+
const ydoc = new Y.Doc();
|
|
211
|
+
const rootMap = ydoc.getMap( 'test' );
|
|
212
|
+
const rootBlocks = new Y.Array< Y.Map< any > >();
|
|
213
|
+
rootMap.set( 'blocks', rootBlocks );
|
|
214
|
+
|
|
215
|
+
// 3 root blocks — the target parent is at index 2.
|
|
216
|
+
const innerArray = new Y.Array< Y.Map< any > >();
|
|
217
|
+
rootBlocks.push( [
|
|
218
|
+
createTestYBlock( 'root-0' ),
|
|
219
|
+
createTestYBlock( 'root-1' ),
|
|
220
|
+
createTestYBlockWithInner( 'root-2', innerArray ),
|
|
221
|
+
] );
|
|
222
|
+
|
|
223
|
+
// 8 inner blocks inside root-2 — the target parent is at index 7.
|
|
224
|
+
const grandchildArray = new Y.Array< Y.Map< any > >();
|
|
225
|
+
const fillerInners: Y.Map< any >[] = [];
|
|
226
|
+
for ( let i = 0; i < 7; i++ ) {
|
|
227
|
+
fillerInners.push( createTestYBlock( `inner-${ i }` ) );
|
|
228
|
+
}
|
|
229
|
+
innerArray.push( [
|
|
230
|
+
...fillerInners,
|
|
231
|
+
createTestYBlockWithInner( 'inner-7', grandchildArray ),
|
|
232
|
+
] );
|
|
233
|
+
|
|
234
|
+
// 2 grandchildren inside inner-7 — the target is at index 1.
|
|
235
|
+
const grandchild = createTestYBlock( 'target' );
|
|
236
|
+
grandchildArray.push( [
|
|
237
|
+
createTestYBlock( 'grandchild-0' ),
|
|
238
|
+
grandchild,
|
|
239
|
+
] );
|
|
240
|
+
|
|
241
|
+
expect( getBlockPathInYdoc( grandchild ) ).toEqual( [ 2, 7, 1 ] );
|
|
242
|
+
} );
|
|
243
|
+
} );
|
|
244
|
+
|
|
245
|
+
describe( 'resolveBlockClientIdByPath', () => {
|
|
246
|
+
afterEach( () => {
|
|
247
|
+
jest.restoreAllMocks();
|
|
248
|
+
} );
|
|
249
|
+
|
|
250
|
+
it( 'should return null for an empty path', () => {
|
|
251
|
+
mockBlockEditorStore( [] );
|
|
252
|
+
expect( resolveBlockClientIdByPath( [] ) ).toBeNull();
|
|
253
|
+
} );
|
|
254
|
+
|
|
255
|
+
it( 'should resolve a root block by single-element path', () => {
|
|
256
|
+
mockBlockEditorStore( [
|
|
257
|
+
{ clientId: 'a', name: 'core/paragraph', innerBlocks: [] },
|
|
258
|
+
{ clientId: 'b', name: 'core/heading', innerBlocks: [] },
|
|
259
|
+
] );
|
|
260
|
+
|
|
261
|
+
expect( resolveBlockClientIdByPath( [ 0 ] ) ).toBe( 'a' );
|
|
262
|
+
expect( resolveBlockClientIdByPath( [ 1 ] ) ).toBe( 'b' );
|
|
263
|
+
} );
|
|
264
|
+
|
|
265
|
+
it( 'should return null for an out-of-bounds index', () => {
|
|
266
|
+
mockBlockEditorStore( [
|
|
267
|
+
{ clientId: 'a', name: 'core/paragraph', innerBlocks: [] },
|
|
268
|
+
] );
|
|
269
|
+
|
|
270
|
+
expect( resolveBlockClientIdByPath( [ 5 ] ) ).toBeNull();
|
|
271
|
+
} );
|
|
272
|
+
|
|
273
|
+
it( 'should resolve a nested inner block', () => {
|
|
274
|
+
mockBlockEditorStore( [
|
|
275
|
+
{
|
|
276
|
+
clientId: 'parent',
|
|
277
|
+
name: 'core/group',
|
|
278
|
+
innerBlocks: [
|
|
279
|
+
{
|
|
280
|
+
clientId: 'child-0',
|
|
281
|
+
name: 'core/paragraph',
|
|
282
|
+
innerBlocks: [],
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
clientId: 'child-1',
|
|
286
|
+
name: 'core/heading',
|
|
287
|
+
innerBlocks: [],
|
|
288
|
+
},
|
|
289
|
+
],
|
|
290
|
+
},
|
|
291
|
+
] );
|
|
292
|
+
|
|
293
|
+
expect( resolveBlockClientIdByPath( [ 0, 1 ] ) ).toBe( 'child-1' );
|
|
294
|
+
} );
|
|
295
|
+
|
|
296
|
+
it( 'should return null when inner path index is out of bounds', () => {
|
|
297
|
+
mockBlockEditorStore( [
|
|
298
|
+
{
|
|
299
|
+
clientId: 'parent',
|
|
300
|
+
name: 'core/group',
|
|
301
|
+
innerBlocks: [
|
|
302
|
+
{
|
|
303
|
+
clientId: 'child-0',
|
|
304
|
+
name: 'core/paragraph',
|
|
305
|
+
innerBlocks: [],
|
|
306
|
+
},
|
|
307
|
+
],
|
|
308
|
+
},
|
|
309
|
+
] );
|
|
310
|
+
|
|
311
|
+
expect( resolveBlockClientIdByPath( [ 0, 5 ] ) ).toBeNull();
|
|
312
|
+
} );
|
|
313
|
+
|
|
314
|
+
describe( 'template mode (getPostContentBlocks behavior)', () => {
|
|
315
|
+
it( 'should navigate through core/post-content in template mode', () => {
|
|
316
|
+
const postContentInnerBlocks: MockBlock[] = [
|
|
317
|
+
{
|
|
318
|
+
clientId: 'post-para-0',
|
|
319
|
+
name: 'core/paragraph',
|
|
320
|
+
innerBlocks: [],
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
clientId: 'post-para-1',
|
|
324
|
+
name: 'core/heading',
|
|
325
|
+
innerBlocks: [],
|
|
326
|
+
},
|
|
327
|
+
];
|
|
328
|
+
|
|
329
|
+
// Template structure: header → post-content → footer.
|
|
330
|
+
// post-content's innerBlocks are empty in the tree (controlled
|
|
331
|
+
// inner blocks), so getBlocks( postContentClientId ) is used.
|
|
332
|
+
const templateBlocks: MockBlock[] = [
|
|
333
|
+
{
|
|
334
|
+
clientId: 'header',
|
|
335
|
+
name: 'core/template-part',
|
|
336
|
+
innerBlocks: [],
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
clientId: 'post-content',
|
|
340
|
+
name: 'core/post-content',
|
|
341
|
+
innerBlocks: [], // Empty — controlled inner blocks.
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
clientId: 'footer',
|
|
345
|
+
name: 'core/template-part',
|
|
346
|
+
innerBlocks: [],
|
|
347
|
+
},
|
|
348
|
+
];
|
|
349
|
+
|
|
350
|
+
const { getBlocks } = mockBlockEditorStore( templateBlocks );
|
|
351
|
+
|
|
352
|
+
// Override getBlocks to return post content blocks when called
|
|
353
|
+
// with the post-content clientId (mimicking controlled inner
|
|
354
|
+
// blocks behavior in useBlockSync).
|
|
355
|
+
getBlocks.mockImplementation( ( rootClientId?: string ) => {
|
|
356
|
+
if ( rootClientId === undefined ) {
|
|
357
|
+
return templateBlocks;
|
|
358
|
+
}
|
|
359
|
+
if ( rootClientId === 'post-content' ) {
|
|
360
|
+
return postContentInnerBlocks;
|
|
361
|
+
}
|
|
362
|
+
return [];
|
|
363
|
+
} );
|
|
364
|
+
|
|
365
|
+
// Path [0] should resolve to the first post content block,
|
|
366
|
+
// not the first template block.
|
|
367
|
+
expect( resolveBlockClientIdByPath( [ 0 ] ) ).toBe( 'post-para-0' );
|
|
368
|
+
expect( resolveBlockClientIdByPath( [ 1 ] ) ).toBe( 'post-para-1' );
|
|
369
|
+
} );
|
|
370
|
+
|
|
371
|
+
it( 'should call getBlocks with post-content clientId', () => {
|
|
372
|
+
const templateBlocks: MockBlock[] = [
|
|
373
|
+
{
|
|
374
|
+
clientId: 'header',
|
|
375
|
+
name: 'core/template-part',
|
|
376
|
+
innerBlocks: [],
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
clientId: 'pc',
|
|
380
|
+
name: 'core/post-content',
|
|
381
|
+
innerBlocks: [],
|
|
382
|
+
},
|
|
383
|
+
];
|
|
384
|
+
|
|
385
|
+
const { getBlocks } = mockBlockEditorStore( templateBlocks );
|
|
386
|
+
getBlocks.mockImplementation( ( rootClientId?: string ) => {
|
|
387
|
+
if ( rootClientId === undefined ) {
|
|
388
|
+
return templateBlocks;
|
|
389
|
+
}
|
|
390
|
+
if ( rootClientId === 'pc' ) {
|
|
391
|
+
return [
|
|
392
|
+
{
|
|
393
|
+
clientId: 'inner',
|
|
394
|
+
name: 'core/paragraph',
|
|
395
|
+
innerBlocks: [],
|
|
396
|
+
},
|
|
397
|
+
];
|
|
398
|
+
}
|
|
399
|
+
return [];
|
|
400
|
+
} );
|
|
401
|
+
|
|
402
|
+
resolveBlockClientIdByPath( [ 0 ] );
|
|
403
|
+
|
|
404
|
+
// Verify getBlocks was called with the post-content clientId.
|
|
405
|
+
expect( getBlocks ).toHaveBeenCalledWith( 'pc' );
|
|
406
|
+
} );
|
|
407
|
+
|
|
408
|
+
it( 'should find core/post-content nested inside template parts', () => {
|
|
409
|
+
// post-content can be nested inside other blocks in the
|
|
410
|
+
// template tree (e.g. inside a group or template part).
|
|
411
|
+
const postContentInnerBlocks: MockBlock[] = [
|
|
412
|
+
{
|
|
413
|
+
clientId: 'deep-para',
|
|
414
|
+
name: 'core/paragraph',
|
|
415
|
+
innerBlocks: [],
|
|
416
|
+
},
|
|
417
|
+
];
|
|
418
|
+
|
|
419
|
+
const templateBlocks: MockBlock[] = [
|
|
420
|
+
{
|
|
421
|
+
clientId: 'group',
|
|
422
|
+
name: 'core/group',
|
|
423
|
+
innerBlocks: [
|
|
424
|
+
{
|
|
425
|
+
clientId: 'nested-pc',
|
|
426
|
+
name: 'core/post-content',
|
|
427
|
+
innerBlocks: [],
|
|
428
|
+
},
|
|
429
|
+
],
|
|
430
|
+
},
|
|
431
|
+
];
|
|
432
|
+
|
|
433
|
+
const { getBlocks } = mockBlockEditorStore( templateBlocks );
|
|
434
|
+
getBlocks.mockImplementation( ( rootClientId?: string ) => {
|
|
435
|
+
if ( rootClientId === undefined ) {
|
|
436
|
+
return templateBlocks;
|
|
437
|
+
}
|
|
438
|
+
if ( rootClientId === 'nested-pc' ) {
|
|
439
|
+
return postContentInnerBlocks;
|
|
440
|
+
}
|
|
441
|
+
return [];
|
|
442
|
+
} );
|
|
443
|
+
|
|
444
|
+
expect( resolveBlockClientIdByPath( [ 0 ] ) ).toBe( 'deep-para' );
|
|
445
|
+
} );
|
|
446
|
+
|
|
447
|
+
it( 'should use root blocks directly when no core/post-content exists', () => {
|
|
448
|
+
// No template mode — plain post editing.
|
|
449
|
+
const blocks: MockBlock[] = [
|
|
450
|
+
{
|
|
451
|
+
clientId: 'para-0',
|
|
452
|
+
name: 'core/paragraph',
|
|
453
|
+
innerBlocks: [],
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
clientId: 'para-1',
|
|
457
|
+
name: 'core/heading',
|
|
458
|
+
innerBlocks: [],
|
|
459
|
+
},
|
|
460
|
+
];
|
|
461
|
+
|
|
462
|
+
mockBlockEditorStore( blocks );
|
|
463
|
+
|
|
464
|
+
expect( resolveBlockClientIdByPath( [ 0 ] ) ).toBe( 'para-0' );
|
|
465
|
+
expect( resolveBlockClientIdByPath( [ 1 ] ) ).toBe( 'para-1' );
|
|
466
|
+
} );
|
|
467
|
+
|
|
468
|
+
it( 'should return null for invalid path in template mode', () => {
|
|
469
|
+
const templateBlocks: MockBlock[] = [
|
|
470
|
+
{
|
|
471
|
+
clientId: 'header',
|
|
472
|
+
name: 'core/template-part',
|
|
473
|
+
innerBlocks: [],
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
clientId: 'pc',
|
|
477
|
+
name: 'core/post-content',
|
|
478
|
+
innerBlocks: [],
|
|
479
|
+
},
|
|
480
|
+
];
|
|
481
|
+
|
|
482
|
+
const { getBlocks } = mockBlockEditorStore( templateBlocks );
|
|
483
|
+
getBlocks.mockImplementation( ( rootClientId?: string ) => {
|
|
484
|
+
if ( rootClientId === undefined ) {
|
|
485
|
+
return templateBlocks;
|
|
486
|
+
}
|
|
487
|
+
if ( rootClientId === 'pc' ) {
|
|
488
|
+
// Post content has only one block.
|
|
489
|
+
return [
|
|
490
|
+
{
|
|
491
|
+
clientId: 'only-block',
|
|
492
|
+
name: 'core/paragraph',
|
|
493
|
+
innerBlocks: [],
|
|
494
|
+
},
|
|
495
|
+
];
|
|
496
|
+
}
|
|
497
|
+
return [];
|
|
498
|
+
} );
|
|
499
|
+
|
|
500
|
+
// Index 1 is out of bounds for the post content blocks.
|
|
501
|
+
expect( resolveBlockClientIdByPath( [ 1 ] ) ).toBeNull();
|
|
502
|
+
} );
|
|
503
|
+
} );
|
|
504
|
+
} );
|