@wordpress/core-data 7.37.1-next.v.0 → 7.38.1-next.v.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 +14 -2
- package/build/actions.cjs.map +2 -2
- package/build/awareness/config.cjs +34 -0
- package/build/awareness/config.cjs.map +7 -0
- package/build/awareness/post-editor-awareness.cjs +144 -0
- package/build/awareness/post-editor-awareness.cjs.map +7 -0
- package/build/awareness/types.cjs +19 -0
- package/build/awareness/types.cjs.map +7 -0
- package/build/awareness/utils.cjs +116 -0
- package/build/awareness/utils.cjs.map +7 -0
- package/build/entities.cjs +48 -45
- package/build/entities.cjs.map +2 -2
- package/build/private-selectors.cjs +2 -4
- package/build/private-selectors.cjs.map +2 -2
- package/build/resolvers.cjs +14 -4
- package/build/resolvers.cjs.map +2 -2
- package/build/types.cjs.map +1 -1
- package/build/utils/crdt-blocks.cjs +63 -41
- package/build/utils/crdt-blocks.cjs.map +2 -2
- package/build/utils/crdt-user-selections.cjs +171 -0
- package/build/utils/crdt-user-selections.cjs.map +7 -0
- package/build/utils/crdt-utils.cjs +44 -0
- package/build/utils/crdt-utils.cjs.map +7 -0
- package/build/utils/crdt.cjs +33 -37
- package/build/utils/crdt.cjs.map +2 -2
- package/build-module/actions.mjs +14 -2
- package/build-module/actions.mjs.map +2 -2
- package/build-module/awareness/config.mjs +8 -0
- package/build-module/awareness/config.mjs.map +7 -0
- package/build-module/awareness/post-editor-awareness.mjs +125 -0
- package/build-module/awareness/post-editor-awareness.mjs.map +7 -0
- package/build-module/awareness/types.mjs +1 -0
- package/build-module/awareness/types.mjs.map +7 -0
- package/build-module/awareness/utils.mjs +90 -0
- package/build-module/awareness/utils.mjs.map +7 -0
- package/build-module/entities.mjs +48 -47
- package/build-module/entities.mjs.map +2 -2
- package/build-module/private-selectors.mjs +2 -4
- package/build-module/private-selectors.mjs.map +2 -2
- package/build-module/resolvers.mjs +14 -4
- package/build-module/resolvers.mjs.map +2 -2
- package/build-module/utils/crdt-blocks.mjs +64 -42
- package/build-module/utils/crdt-blocks.mjs.map +2 -2
- package/build-module/utils/crdt-user-selections.mjs +144 -0
- package/build-module/utils/crdt-user-selections.mjs.map +7 -0
- package/build-module/utils/crdt-utils.mjs +17 -0
- package/build-module/utils/crdt-utils.mjs.map +7 -0
- package/build-module/utils/crdt.mjs +37 -37
- package/build-module/utils/crdt.mjs.map +2 -2
- package/build-types/actions.d.ts.map +1 -1
- package/build-types/awareness/config.d.ts +9 -0
- package/build-types/awareness/config.d.ts.map +1 -0
- package/build-types/awareness/post-editor-awareness.d.ts +39 -0
- package/build-types/awareness/post-editor-awareness.d.ts.map +1 -0
- package/build-types/awareness/types.d.ts +32 -0
- package/build-types/awareness/types.d.ts.map +1 -0
- package/build-types/awareness/utils.d.ts +22 -0
- package/build-types/awareness/utils.d.ts.map +1 -0
- package/build-types/entities.d.ts.map +1 -1
- package/build-types/index.d.ts.map +1 -1
- package/build-types/private-selectors.d.ts.map +1 -1
- package/build-types/resolvers.d.ts.map +1 -1
- package/build-types/types.d.ts +0 -5
- package/build-types/types.d.ts.map +1 -1
- package/build-types/utils/crdt-blocks.d.ts +14 -5
- package/build-types/utils/crdt-blocks.d.ts.map +1 -1
- package/build-types/utils/crdt-user-selections.d.ts +66 -0
- package/build-types/utils/crdt-user-selections.d.ts.map +1 -0
- package/build-types/utils/crdt-utils.d.ts +53 -0
- package/build-types/utils/crdt-utils.d.ts.map +1 -0
- package/build-types/utils/crdt.d.ts +19 -1
- package/build-types/utils/crdt.d.ts.map +1 -1
- package/package.json +19 -18
- package/src/actions.js +13 -2
- package/src/awareness/config.ts +9 -0
- package/src/awareness/post-editor-awareness.ts +187 -0
- package/src/awareness/types.ts +38 -0
- package/src/awareness/utils.ts +159 -0
- package/src/entities.js +52 -54
- package/src/private-selectors.ts +3 -4
- package/src/resolvers.js +17 -8
- package/src/test/entities.js +11 -9
- package/src/test/entity-provider.js +2 -0
- package/src/test/resolvers.js +3 -45
- package/src/types.ts +0 -6
- package/src/utils/crdt-blocks.ts +101 -99
- package/src/utils/crdt-user-selections.ts +332 -0
- package/src/utils/crdt-utils.ts +77 -0
- package/src/utils/crdt.ts +76 -57
- package/src/utils/test/crdt-blocks.ts +11 -4
- package/src/utils/test/crdt.ts +28 -16
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External dependencies
|
|
3
|
+
*/
|
|
4
|
+
import { Y, CRDT_RECORD_MAP_KEY } from '@wordpress/sync';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Internal dependencies
|
|
8
|
+
*/
|
|
9
|
+
import type { YPostRecord } from './crdt';
|
|
10
|
+
import type { YBlock, YBlocks } from './crdt-blocks';
|
|
11
|
+
import { getRootMap } from './crdt-utils';
|
|
12
|
+
import type { WPBlockSelection } from '../types';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The type of selection.
|
|
16
|
+
*/
|
|
17
|
+
export enum SelectionType {
|
|
18
|
+
None = 'none',
|
|
19
|
+
Cursor = 'cursor',
|
|
20
|
+
SelectionInOneBlock = 'selection-in-one-block',
|
|
21
|
+
SelectionInMultipleBlocks = 'selection-in-multiple-blocks',
|
|
22
|
+
WholeBlock = 'whole-block',
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The position of the cursor.
|
|
27
|
+
*/
|
|
28
|
+
export type CursorPosition = {
|
|
29
|
+
relativePosition: Y.RelativePosition;
|
|
30
|
+
|
|
31
|
+
// Also store the absolute offset index of the cursor from the perspective
|
|
32
|
+
// of the user who is updating the selection.
|
|
33
|
+
//
|
|
34
|
+
// Do not use this value directly, instead use `createAbsolutePositionFromRelativePosition()`
|
|
35
|
+
// on relativePosition for the most up-to-date positioning.
|
|
36
|
+
//
|
|
37
|
+
// This is used because local Y.Text changes (e.g. adding or deleting a character)
|
|
38
|
+
// can result in the same relative position if it is pinned to an unchanged
|
|
39
|
+
// character. With both of these values as editor state, a change in perceived
|
|
40
|
+
// position will always result in a redraw.
|
|
41
|
+
absoluteOffset: number;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type SelectionNone = {
|
|
45
|
+
// The user has not made a selection.
|
|
46
|
+
type: SelectionType.None;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export type SelectionCursor = {
|
|
50
|
+
// The user has a cursor position in a block with no text highlighted.
|
|
51
|
+
type: SelectionType.Cursor;
|
|
52
|
+
blockId: string;
|
|
53
|
+
cursorPosition: CursorPosition;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export type SelectionInOneBlock = {
|
|
57
|
+
// The user has highlighted text in a single block.
|
|
58
|
+
type: SelectionType.SelectionInOneBlock;
|
|
59
|
+
blockId: string;
|
|
60
|
+
cursorStartPosition: CursorPosition;
|
|
61
|
+
cursorEndPosition: CursorPosition;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export type SelectionInMultipleBlocks = {
|
|
65
|
+
// The user has highlighted text over multiple blocks.
|
|
66
|
+
type: SelectionType.SelectionInMultipleBlocks;
|
|
67
|
+
blockStartId: string;
|
|
68
|
+
blockEndId: string;
|
|
69
|
+
cursorStartPosition: CursorPosition;
|
|
70
|
+
cursorEndPosition: CursorPosition;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export type SelectionWholeBlock = {
|
|
74
|
+
// The user has a non-text block selected, like an image block.
|
|
75
|
+
type: SelectionType.WholeBlock;
|
|
76
|
+
blockId: string;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export type SelectionState =
|
|
80
|
+
| SelectionNone
|
|
81
|
+
| SelectionCursor
|
|
82
|
+
| SelectionInOneBlock
|
|
83
|
+
| SelectionInMultipleBlocks
|
|
84
|
+
| SelectionWholeBlock;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Converts WordPress block editor selection to a SelectionState.
|
|
88
|
+
*
|
|
89
|
+
* @param selectionStart - The start position of the selection
|
|
90
|
+
* @param selectionEnd - The end position of the selection
|
|
91
|
+
* @param yDoc - The Yjs document
|
|
92
|
+
* @return The SelectionState
|
|
93
|
+
*/
|
|
94
|
+
export function getSelectionState(
|
|
95
|
+
selectionStart: WPBlockSelection,
|
|
96
|
+
selectionEnd: WPBlockSelection,
|
|
97
|
+
yDoc: Y.Doc
|
|
98
|
+
): SelectionState {
|
|
99
|
+
const ymap = getRootMap< YPostRecord >( yDoc, CRDT_RECORD_MAP_KEY );
|
|
100
|
+
const yBlocks = ymap.get( 'blocks' ) ?? new Y.Array< YBlock >();
|
|
101
|
+
|
|
102
|
+
const isSelectionEmpty = Object.keys( selectionStart ).length === 0;
|
|
103
|
+
const noSelection: SelectionNone = {
|
|
104
|
+
type: SelectionType.None,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
if ( isSelectionEmpty ) {
|
|
108
|
+
// Case 1: No selection
|
|
109
|
+
return noSelection;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// When the page initially loads, selectionStart can contain an empty object `{}`.
|
|
113
|
+
const isSelectionInOneBlock =
|
|
114
|
+
selectionStart.clientId === selectionEnd.clientId;
|
|
115
|
+
const isCursorOnly =
|
|
116
|
+
isSelectionInOneBlock && selectionStart.offset === selectionEnd.offset;
|
|
117
|
+
const isSelectionAWholeBlock =
|
|
118
|
+
isSelectionInOneBlock &&
|
|
119
|
+
selectionStart.offset === undefined &&
|
|
120
|
+
selectionEnd.offset === undefined;
|
|
121
|
+
|
|
122
|
+
if ( isSelectionAWholeBlock ) {
|
|
123
|
+
// Case 2: A whole block is selected.
|
|
124
|
+
return {
|
|
125
|
+
type: SelectionType.WholeBlock,
|
|
126
|
+
blockId: selectionStart.clientId,
|
|
127
|
+
};
|
|
128
|
+
} else if ( isCursorOnly ) {
|
|
129
|
+
// Case 3: Cursor only, no text selected
|
|
130
|
+
const cursorPosition = getCursorPosition( selectionStart, yBlocks );
|
|
131
|
+
|
|
132
|
+
if ( ! cursorPosition ) {
|
|
133
|
+
// If we can't find the cursor position in block text, treat it as a non-selection.
|
|
134
|
+
return noSelection;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
type: SelectionType.Cursor,
|
|
139
|
+
blockId: selectionStart.clientId,
|
|
140
|
+
cursorPosition,
|
|
141
|
+
};
|
|
142
|
+
} else if ( isSelectionInOneBlock ) {
|
|
143
|
+
// Case 4: Selection in a single block
|
|
144
|
+
const cursorStartPosition = getCursorPosition(
|
|
145
|
+
selectionStart,
|
|
146
|
+
yBlocks
|
|
147
|
+
);
|
|
148
|
+
const cursorEndPosition = getCursorPosition( selectionEnd, yBlocks );
|
|
149
|
+
|
|
150
|
+
if ( ! cursorStartPosition || ! cursorEndPosition ) {
|
|
151
|
+
// If we can't find the cursor positions in block text, treat it as a non-selection.
|
|
152
|
+
return noSelection;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
type: SelectionType.SelectionInOneBlock,
|
|
157
|
+
blockId: selectionStart.clientId,
|
|
158
|
+
cursorStartPosition,
|
|
159
|
+
cursorEndPosition,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Caes 5: Selection in multiple blocks
|
|
164
|
+
const cursorStartPosition = getCursorPosition( selectionStart, yBlocks );
|
|
165
|
+
const cursorEndPosition = getCursorPosition( selectionEnd, yBlocks );
|
|
166
|
+
if ( ! cursorStartPosition || ! cursorEndPosition ) {
|
|
167
|
+
// If we can't find the cursor positions in block text, treat it as a non-selection.
|
|
168
|
+
return noSelection;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
type: SelectionType.SelectionInMultipleBlocks,
|
|
173
|
+
blockStartId: selectionStart.clientId,
|
|
174
|
+
blockEndId: selectionEnd.clientId,
|
|
175
|
+
cursorStartPosition,
|
|
176
|
+
cursorEndPosition,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Get the cursor position from a selection.
|
|
182
|
+
*
|
|
183
|
+
* @param selection - The selection.
|
|
184
|
+
* @param blocks - The blocks to search through.
|
|
185
|
+
* @return The cursor position, or null if not found.
|
|
186
|
+
*/
|
|
187
|
+
function getCursorPosition(
|
|
188
|
+
selection: WPBlockSelection,
|
|
189
|
+
blocks: YBlocks
|
|
190
|
+
): CursorPosition | null {
|
|
191
|
+
const block = findBlockByClientId( selection.clientId, blocks );
|
|
192
|
+
if ( ! block ) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const attributes = block.get( 'attributes' ) as Y.Map< Y.Text >;
|
|
197
|
+
const currentYText = attributes.get( selection.attributeKey ) as Y.Text;
|
|
198
|
+
|
|
199
|
+
const relativePosition = Y.createRelativePositionFromTypeIndex(
|
|
200
|
+
currentYText,
|
|
201
|
+
selection.offset
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
relativePosition,
|
|
206
|
+
absoluteOffset: selection.offset,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Find a block by its client ID.
|
|
212
|
+
*
|
|
213
|
+
* @param blockId - The client ID of the block.
|
|
214
|
+
* @param blocks - The blocks to search through.
|
|
215
|
+
* @return The block if found, null otherwise.
|
|
216
|
+
*/
|
|
217
|
+
function findBlockByClientId(
|
|
218
|
+
blockId: string,
|
|
219
|
+
blocks: YBlocks
|
|
220
|
+
): YBlock | null {
|
|
221
|
+
for ( const block of blocks ) {
|
|
222
|
+
if ( block.get( 'clientId' ) === blockId ) {
|
|
223
|
+
return block;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const innerBlocks = block.get( 'innerBlocks' );
|
|
227
|
+
|
|
228
|
+
if ( innerBlocks && innerBlocks.length > 0 ) {
|
|
229
|
+
const innerBlock = findBlockByClientId( blockId, innerBlocks );
|
|
230
|
+
|
|
231
|
+
if ( innerBlock ) {
|
|
232
|
+
return innerBlock;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Check if two selection states are equal.
|
|
242
|
+
*
|
|
243
|
+
* @param selection1 - The first selection state.
|
|
244
|
+
* @param selection2 - The second selection state.
|
|
245
|
+
* @return True if the selection states are equal, false otherwise.
|
|
246
|
+
*/
|
|
247
|
+
export function areSelectionsStatesEqual(
|
|
248
|
+
selection1: SelectionState,
|
|
249
|
+
selection2: SelectionState
|
|
250
|
+
): boolean {
|
|
251
|
+
if ( selection1.type !== selection2.type ) {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
switch ( selection1.type ) {
|
|
256
|
+
case SelectionType.None:
|
|
257
|
+
return true;
|
|
258
|
+
|
|
259
|
+
case SelectionType.Cursor:
|
|
260
|
+
return (
|
|
261
|
+
selection1.blockId ===
|
|
262
|
+
( selection2 as SelectionCursor ).blockId &&
|
|
263
|
+
areCursorPositionsEqual(
|
|
264
|
+
selection1.cursorPosition,
|
|
265
|
+
( selection2 as SelectionCursor ).cursorPosition
|
|
266
|
+
)
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
case SelectionType.SelectionInOneBlock:
|
|
270
|
+
return (
|
|
271
|
+
selection1.blockId ===
|
|
272
|
+
( selection2 as SelectionInOneBlock ).blockId &&
|
|
273
|
+
areCursorPositionsEqual(
|
|
274
|
+
selection1.cursorStartPosition,
|
|
275
|
+
( selection2 as SelectionInOneBlock ).cursorStartPosition
|
|
276
|
+
) &&
|
|
277
|
+
areCursorPositionsEqual(
|
|
278
|
+
selection1.cursorEndPosition,
|
|
279
|
+
( selection2 as SelectionInOneBlock ).cursorEndPosition
|
|
280
|
+
)
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
case SelectionType.SelectionInMultipleBlocks:
|
|
284
|
+
return (
|
|
285
|
+
selection1.blockStartId ===
|
|
286
|
+
( selection2 as SelectionInMultipleBlocks ).blockStartId &&
|
|
287
|
+
selection1.blockEndId ===
|
|
288
|
+
( selection2 as SelectionInMultipleBlocks ).blockEndId &&
|
|
289
|
+
areCursorPositionsEqual(
|
|
290
|
+
selection1.cursorStartPosition,
|
|
291
|
+
( selection2 as SelectionInMultipleBlocks )
|
|
292
|
+
.cursorStartPosition
|
|
293
|
+
) &&
|
|
294
|
+
areCursorPositionsEqual(
|
|
295
|
+
selection1.cursorEndPosition,
|
|
296
|
+
( selection2 as SelectionInMultipleBlocks )
|
|
297
|
+
.cursorEndPosition
|
|
298
|
+
)
|
|
299
|
+
);
|
|
300
|
+
case SelectionType.WholeBlock:
|
|
301
|
+
return (
|
|
302
|
+
selection1.blockId ===
|
|
303
|
+
( selection2 as SelectionWholeBlock ).blockId
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
default:
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Check if two cursor positions are equal.
|
|
313
|
+
*
|
|
314
|
+
* @param cursorPosition1 - The first cursor position.
|
|
315
|
+
* @param cursorPosition2 - The second cursor position.
|
|
316
|
+
* @return True if the cursor positions are equal, false otherwise.
|
|
317
|
+
*/
|
|
318
|
+
function areCursorPositionsEqual(
|
|
319
|
+
cursorPosition1: CursorPosition,
|
|
320
|
+
cursorPosition2: CursorPosition
|
|
321
|
+
): boolean {
|
|
322
|
+
const isRelativePositionEqual =
|
|
323
|
+
JSON.stringify( cursorPosition1.relativePosition ) ===
|
|
324
|
+
JSON.stringify( cursorPosition2.relativePosition );
|
|
325
|
+
|
|
326
|
+
// Ensure a change in calculated absolute offset results in a treating the cursor as modified.
|
|
327
|
+
// This is necessary because Y.Text relative positions can remain the same after text changes.
|
|
328
|
+
const isAbsoluteOffsetEqual =
|
|
329
|
+
cursorPosition1.absoluteOffset === cursorPosition2.absoluteOffset;
|
|
330
|
+
|
|
331
|
+
return isRelativePositionEqual && isAbsoluteOffsetEqual;
|
|
332
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress dependencies
|
|
3
|
+
*/
|
|
4
|
+
import { Y } from '@wordpress/sync';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A YMapRecord represents the shape of the data stored in a Y.Map.
|
|
8
|
+
*/
|
|
9
|
+
export type YMapRecord = Record< string, unknown >;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A wrapper around Y.Map to provide type safety. The generic type accepted by
|
|
13
|
+
* Y.Map represents the union of possible values of the map, which are varied in
|
|
14
|
+
* many cases. This type is accurate, but its non-specificity requires aggressive
|
|
15
|
+
* type narrowing or type casting / destruction with `as`.
|
|
16
|
+
*
|
|
17
|
+
* This type provides type enhancements so that the correct value type can be
|
|
18
|
+
* inferred based on the provided key. It is just a type wrap / overlay, and
|
|
19
|
+
* does not change the runtime behavior of Y.Map.
|
|
20
|
+
*
|
|
21
|
+
* This interface cannot extend Y.Map directly due to the limitations of
|
|
22
|
+
* TypeScript's structural typing. One negative consequence of this is that
|
|
23
|
+
* `instanceof` checks against Y.Map continue to work at runtime but will blur
|
|
24
|
+
* the type at compile time. To navigate this, use the `isYMap` function below.
|
|
25
|
+
*/
|
|
26
|
+
export interface YMapWrap< T extends YMapRecord > extends Y.AbstractType< T > {
|
|
27
|
+
delete: < K extends keyof T >( key: K ) => void;
|
|
28
|
+
forEach: (
|
|
29
|
+
callback: (
|
|
30
|
+
value: T[ keyof T ],
|
|
31
|
+
key: keyof T,
|
|
32
|
+
map: YMapWrap< T >
|
|
33
|
+
) => void
|
|
34
|
+
) => void;
|
|
35
|
+
has: < K extends keyof T >( key: K ) => boolean;
|
|
36
|
+
get: < K extends keyof T >( key: K ) => T[ K ] | undefined;
|
|
37
|
+
set: < K extends keyof T >( key: K, value: T[ K ] ) => void;
|
|
38
|
+
toJSON: () => T;
|
|
39
|
+
// add types for other Y.Map methods as needed
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get or create a root-level Map for the given Y.Doc. Use this instead of
|
|
44
|
+
* doc.getMap() for additional type safety.
|
|
45
|
+
*
|
|
46
|
+
* @param doc Y.Doc
|
|
47
|
+
* @param key Map key
|
|
48
|
+
*/
|
|
49
|
+
export function getRootMap< T extends YMapRecord >(
|
|
50
|
+
doc: Y.Doc,
|
|
51
|
+
key: string
|
|
52
|
+
): YMapWrap< T > {
|
|
53
|
+
return doc.getMap< T >( key ) as unknown as YMapWrap< T >;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Create a new Y.Map (provided with YMapWrap type), optionally initialized with
|
|
58
|
+
* data. Use this instead of `new Y.Map()` for additional type safety.
|
|
59
|
+
*
|
|
60
|
+
* @param partial Partial data to initialize the map with.
|
|
61
|
+
*/
|
|
62
|
+
export function createYMap< T extends YMapRecord >(
|
|
63
|
+
partial: Partial< T > = {}
|
|
64
|
+
): YMapWrap< T > {
|
|
65
|
+
return new Y.Map( Object.entries( partial ) ) as unknown as YMapWrap< T >;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Type guard to check if a value is a Y.Map without losing type information.
|
|
70
|
+
*
|
|
71
|
+
* @param value Value to check.
|
|
72
|
+
*/
|
|
73
|
+
export function isYMap< T extends YMapRecord >(
|
|
74
|
+
value: YMapWrap< T > | undefined
|
|
75
|
+
): value is YMapWrap< T > {
|
|
76
|
+
return value instanceof Y.Map;
|
|
77
|
+
}
|
package/src/utils/crdt.ts
CHANGED
|
@@ -26,8 +26,16 @@ import {
|
|
|
26
26
|
CRDT_RECORD_MAP_KEY,
|
|
27
27
|
WORDPRESS_META_KEY_FOR_CRDT_DOC_PERSISTENCE,
|
|
28
28
|
} from '../sync';
|
|
29
|
-
import type {
|
|
30
|
-
|
|
29
|
+
import type { WPSelection } from '../types';
|
|
30
|
+
import {
|
|
31
|
+
createYMap,
|
|
32
|
+
getRootMap,
|
|
33
|
+
isYMap,
|
|
34
|
+
type YMapRecord,
|
|
35
|
+
type YMapWrap,
|
|
36
|
+
} from './crdt-utils';
|
|
37
|
+
|
|
38
|
+
// Changes that can be applied to a post entity record.
|
|
31
39
|
export type PostChanges = Partial< Post > & {
|
|
32
40
|
blocks?: Block[];
|
|
33
41
|
excerpt?: Post[ 'excerpt' ] | string;
|
|
@@ -35,8 +43,24 @@ export type PostChanges = Partial< Post > & {
|
|
|
35
43
|
title?: Post[ 'title' ] | string;
|
|
36
44
|
};
|
|
37
45
|
|
|
38
|
-
//
|
|
39
|
-
|
|
46
|
+
// A post record as represented in the CRDT document (Y.Map).
|
|
47
|
+
export interface YPostRecord extends YMapRecord {
|
|
48
|
+
author: number;
|
|
49
|
+
blocks: YBlocks;
|
|
50
|
+
comment_status: string;
|
|
51
|
+
date: string | null;
|
|
52
|
+
excerpt: string;
|
|
53
|
+
featured_media: number;
|
|
54
|
+
format: string;
|
|
55
|
+
meta: YMapWrap< YMapRecord >;
|
|
56
|
+
ping_status: string;
|
|
57
|
+
slug: string;
|
|
58
|
+
status: string;
|
|
59
|
+
sticky: boolean;
|
|
60
|
+
tags: number[];
|
|
61
|
+
template: string;
|
|
62
|
+
title: string;
|
|
63
|
+
}
|
|
40
64
|
|
|
41
65
|
// Properties that are allowed to be synced for a post.
|
|
42
66
|
const allowedPostProperties = new Set< string >( [
|
|
@@ -47,8 +71,8 @@ const allowedPostProperties = new Set< string >( [
|
|
|
47
71
|
'excerpt',
|
|
48
72
|
'featured_media',
|
|
49
73
|
'format',
|
|
50
|
-
'ping_status',
|
|
51
74
|
'meta',
|
|
75
|
+
'ping_status',
|
|
52
76
|
'slug',
|
|
53
77
|
'status',
|
|
54
78
|
'sticky',
|
|
@@ -74,7 +98,7 @@ export function defaultApplyChangesToCRDTDoc(
|
|
|
74
98
|
ydoc: CRDTDoc,
|
|
75
99
|
changes: ObjectData
|
|
76
100
|
): void {
|
|
77
|
-
const ymap =
|
|
101
|
+
const ymap = getRootMap( ydoc, CRDT_RECORD_MAP_KEY );
|
|
78
102
|
|
|
79
103
|
Object.entries( changes ).forEach( ( [ key, newValue ] ) => {
|
|
80
104
|
// Cannot serialize function values, so cannot sync them.
|
|
@@ -82,17 +106,12 @@ export function defaultApplyChangesToCRDTDoc(
|
|
|
82
106
|
return;
|
|
83
107
|
}
|
|
84
108
|
|
|
85
|
-
// Set the value in the root document.
|
|
86
|
-
function setValue< T = unknown >( updatedValue: T ): void {
|
|
87
|
-
ymap.set( key, updatedValue );
|
|
88
|
-
}
|
|
89
|
-
|
|
90
109
|
switch ( key ) {
|
|
91
110
|
// Add support for additional data types here.
|
|
92
111
|
|
|
93
112
|
default: {
|
|
94
113
|
const currentValue = ymap.get( key );
|
|
95
|
-
|
|
114
|
+
updateMapValue( ymap, key, currentValue, newValue );
|
|
96
115
|
}
|
|
97
116
|
}
|
|
98
117
|
} );
|
|
@@ -112,60 +131,60 @@ export function applyPostChangesToCRDTDoc(
|
|
|
112
131
|
changes: PostChanges,
|
|
113
132
|
_postType: Type // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
114
133
|
): void {
|
|
115
|
-
const ymap =
|
|
134
|
+
const ymap = getRootMap< YPostRecord >( ydoc, CRDT_RECORD_MAP_KEY );
|
|
116
135
|
|
|
117
|
-
Object.
|
|
136
|
+
Object.keys( changes ).forEach( ( key ) => {
|
|
118
137
|
if ( ! allowedPostProperties.has( key ) ) {
|
|
119
138
|
return;
|
|
120
139
|
}
|
|
121
140
|
|
|
141
|
+
const newValue = changes[ key ];
|
|
142
|
+
|
|
122
143
|
// Cannot serialize function values, so cannot sync them.
|
|
123
144
|
if ( 'function' === typeof newValue ) {
|
|
124
145
|
return;
|
|
125
146
|
}
|
|
126
147
|
|
|
127
|
-
// Set the value in the root document.
|
|
128
|
-
function setValue< T = unknown >( updatedValue: T ): void {
|
|
129
|
-
ymap.set( key, updatedValue );
|
|
130
|
-
}
|
|
131
|
-
|
|
132
148
|
switch ( key ) {
|
|
133
149
|
case 'blocks': {
|
|
134
|
-
let currentBlocks = ymap.get(
|
|
150
|
+
let currentBlocks = ymap.get( key );
|
|
135
151
|
|
|
136
152
|
// Initialize.
|
|
137
153
|
if ( ! ( currentBlocks instanceof Y.Array ) ) {
|
|
138
154
|
currentBlocks = new Y.Array< YBlock >();
|
|
139
|
-
|
|
155
|
+
ymap.set( key, currentBlocks );
|
|
140
156
|
}
|
|
141
157
|
|
|
142
158
|
// Block[] from local changes.
|
|
143
159
|
const newBlocks = ( newValue as PostChanges[ 'blocks' ] ) ?? [];
|
|
144
160
|
|
|
161
|
+
// Block changes from typing are bundled with a 'selection' update.
|
|
162
|
+
// Pass the resulting cursor position to the mergeCrdtBlocks function.
|
|
163
|
+
const cursorPosition =
|
|
164
|
+
changes.selection?.selectionStart?.offset ?? null;
|
|
165
|
+
|
|
145
166
|
// Merge blocks does not need `setValue` because it is operating on a
|
|
146
167
|
// Yjs type that is already in the Y.Doc.
|
|
147
|
-
mergeCrdtBlocks( currentBlocks, newBlocks,
|
|
168
|
+
mergeCrdtBlocks( currentBlocks, newBlocks, cursorPosition );
|
|
148
169
|
break;
|
|
149
170
|
}
|
|
150
171
|
|
|
151
172
|
case 'excerpt': {
|
|
152
|
-
const currentValue = ymap.get( 'excerpt' )
|
|
153
|
-
| string
|
|
154
|
-
| undefined;
|
|
173
|
+
const currentValue = ymap.get( 'excerpt' );
|
|
155
174
|
const rawNewValue = getRawValue( newValue );
|
|
156
175
|
|
|
157
|
-
|
|
176
|
+
updateMapValue( ymap, key, currentValue, rawNewValue );
|
|
158
177
|
break;
|
|
159
178
|
}
|
|
160
179
|
|
|
161
180
|
// "Meta" is overloaded term; here, it refers to post meta.
|
|
162
181
|
case 'meta': {
|
|
163
|
-
let metaMap = ymap.get( 'meta' )
|
|
182
|
+
let metaMap = ymap.get( 'meta' );
|
|
164
183
|
|
|
165
184
|
// Initialize.
|
|
166
|
-
if ( ! ( metaMap
|
|
167
|
-
metaMap =
|
|
168
|
-
|
|
185
|
+
if ( ! isYMap( metaMap ) ) {
|
|
186
|
+
metaMap = createYMap< YMapRecord >();
|
|
187
|
+
ymap.set( 'meta', metaMap );
|
|
169
188
|
}
|
|
170
189
|
|
|
171
190
|
// Iterate over each meta property in the new value and merge it if it
|
|
@@ -176,12 +195,11 @@ export function applyPostChangesToCRDTDoc(
|
|
|
176
195
|
return;
|
|
177
196
|
}
|
|
178
197
|
|
|
179
|
-
|
|
198
|
+
updateMapValue(
|
|
199
|
+
metaMap,
|
|
200
|
+
metaKey,
|
|
180
201
|
metaMap.get( metaKey ), // current value in CRDT
|
|
181
|
-
metaValue
|
|
182
|
-
( updatedMetaValue: unknown ): void => {
|
|
183
|
-
metaMap.set( metaKey, updatedMetaValue );
|
|
184
|
-
}
|
|
202
|
+
metaValue // new value from changes
|
|
185
203
|
);
|
|
186
204
|
}
|
|
187
205
|
);
|
|
@@ -195,13 +213,13 @@ export function applyPostChangesToCRDTDoc(
|
|
|
195
213
|
break;
|
|
196
214
|
}
|
|
197
215
|
|
|
198
|
-
const currentValue = ymap.get(
|
|
199
|
-
|
|
216
|
+
const currentValue = ymap.get( key );
|
|
217
|
+
updateMapValue( ymap, key, currentValue, newValue );
|
|
200
218
|
break;
|
|
201
219
|
}
|
|
202
220
|
|
|
203
221
|
case 'title': {
|
|
204
|
-
const currentValue = ymap.get(
|
|
222
|
+
const currentValue = ymap.get( key );
|
|
205
223
|
|
|
206
224
|
// Copy logic from prePersistPostType to ensure that the "Auto
|
|
207
225
|
// Draft" template title is not synced.
|
|
@@ -210,27 +228,22 @@ export function applyPostChangesToCRDTDoc(
|
|
|
210
228
|
rawNewValue = '';
|
|
211
229
|
}
|
|
212
230
|
|
|
213
|
-
|
|
231
|
+
updateMapValue( ymap, key, currentValue, rawNewValue );
|
|
214
232
|
break;
|
|
215
233
|
}
|
|
216
234
|
|
|
217
|
-
// Add support for additional
|
|
235
|
+
// Add support for additional properties here.
|
|
218
236
|
|
|
219
237
|
default: {
|
|
220
238
|
const currentValue = ymap.get( key );
|
|
221
|
-
|
|
239
|
+
updateMapValue( ymap, key, currentValue, newValue );
|
|
222
240
|
}
|
|
223
241
|
}
|
|
224
242
|
} );
|
|
225
|
-
|
|
226
|
-
// Update the lastSelection for use in computing Y.Text deltas.
|
|
227
|
-
if ( 'selection' in changes ) {
|
|
228
|
-
lastSelection = changes.selection?.selectionStart ?? null;
|
|
229
|
-
}
|
|
230
243
|
}
|
|
231
244
|
|
|
232
245
|
export function defaultGetChangesFromCRDTDoc( crdtDoc: CRDTDoc ): ObjectData {
|
|
233
|
-
return
|
|
246
|
+
return getRootMap( crdtDoc, CRDT_RECORD_MAP_KEY ).toJSON();
|
|
234
247
|
}
|
|
235
248
|
|
|
236
249
|
/**
|
|
@@ -248,7 +261,7 @@ export function getPostChangesFromCRDTDoc(
|
|
|
248
261
|
editedRecord: Post,
|
|
249
262
|
_postType: Type // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
250
263
|
): PostChanges {
|
|
251
|
-
const ymap =
|
|
264
|
+
const ymap = getRootMap< YPostRecord >( ydoc, CRDT_RECORD_MAP_KEY );
|
|
252
265
|
|
|
253
266
|
let allowedMetaChanges: Post[ 'meta' ] = {};
|
|
254
267
|
|
|
@@ -394,19 +407,25 @@ function getRawValue( value?: unknown ): string | undefined {
|
|
|
394
407
|
return undefined;
|
|
395
408
|
}
|
|
396
409
|
|
|
397
|
-
function haveValuesChanged< ValueType
|
|
398
|
-
currentValue: ValueType,
|
|
399
|
-
newValue: ValueType
|
|
410
|
+
function haveValuesChanged< ValueType >(
|
|
411
|
+
currentValue: ValueType | undefined,
|
|
412
|
+
newValue: ValueType | undefined
|
|
400
413
|
): boolean {
|
|
401
414
|
return ! fastDeepEqual( currentValue, newValue );
|
|
402
415
|
}
|
|
403
416
|
|
|
404
|
-
function
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
417
|
+
function updateMapValue< T extends YMapRecord, K extends keyof T >(
|
|
418
|
+
map: YMapWrap< T >,
|
|
419
|
+
key: K,
|
|
420
|
+
currentValue: T[ K ] | undefined,
|
|
421
|
+
newValue: T[ K ] | undefined
|
|
408
422
|
): void {
|
|
409
|
-
if (
|
|
410
|
-
|
|
423
|
+
if ( undefined === newValue ) {
|
|
424
|
+
map.delete( key );
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if ( haveValuesChanged< T[ K ] >( currentValue, newValue ) ) {
|
|
429
|
+
map.set( key, newValue );
|
|
411
430
|
}
|
|
412
431
|
}
|