@wordpress/core-data 7.38.0 → 7.39.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 +17 -6
- package/build/actions.cjs.map +2 -2
- package/build/awareness/base-awareness.cjs +62 -0
- package/build/awareness/base-awareness.cjs.map +7 -0
- package/build/awareness/config.cjs +34 -0
- package/build/awareness/config.cjs.map +7 -0
- package/build/awareness/post-editor-awareness.cjs +130 -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 +27 -2
- package/build/entities.cjs.map +2 -2
- package/build/resolvers.cjs +43 -2
- package/build/resolvers.cjs.map +2 -2
- package/build/types.cjs.map +1 -1
- package/build/utils/block-selection-history.cjs +101 -0
- package/build/utils/block-selection-history.cjs.map +7 -0
- package/build/utils/crdt-selection.cjs +139 -0
- package/build/utils/crdt-selection.cjs.map +7 -0
- 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 +29 -0
- package/build/utils/crdt-utils.cjs.map +3 -3
- package/build/utils/crdt.cjs +16 -4
- package/build/utils/crdt.cjs.map +2 -2
- package/build-module/actions.mjs +17 -6
- package/build-module/actions.mjs.map +2 -2
- package/build-module/awareness/base-awareness.mjs +35 -0
- package/build-module/awareness/base-awareness.mjs.map +7 -0
- 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 +111 -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 +28 -2
- package/build-module/entities.mjs.map +2 -2
- package/build-module/resolvers.mjs +43 -2
- package/build-module/resolvers.mjs.map +2 -2
- package/build-module/utils/block-selection-history.mjs +75 -0
- package/build-module/utils/block-selection-history.mjs.map +7 -0
- package/build-module/utils/crdt-selection.mjs +115 -0
- package/build-module/utils/crdt-selection.mjs.map +7 -0
- 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 +28 -0
- package/build-module/utils/crdt-utils.mjs.map +2 -2
- package/build-module/utils/crdt.mjs +18 -3
- package/build-module/utils/crdt.mjs.map +2 -2
- package/build-types/actions.d.ts.map +1 -1
- package/build-types/awareness/base-awareness.d.ts +19 -0
- package/build-types/awareness/base-awareness.d.ts.map +1 -0
- 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 +38 -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/entity-types/test/attachment.test.d.ts +10 -0
- package/build-types/entity-types/test/attachment.test.d.ts.map +1 -0
- package/build-types/index.d.ts.map +1 -1
- package/build-types/resolvers.d.ts.map +1 -1
- package/build-types/types.d.ts +12 -2
- package/build-types/types.d.ts.map +1 -1
- package/build-types/utils/block-selection-history.d.ts +47 -0
- package/build-types/utils/block-selection-history.d.ts.map +1 -0
- package/build-types/utils/crdt-selection.d.ts +16 -0
- package/build-types/utils/crdt-selection.d.ts.map +1 -0
- 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 +12 -0
- package/build-types/utils/crdt-utils.d.ts.map +1 -1
- package/build-types/utils/crdt.d.ts +8 -14
- package/build-types/utils/crdt.d.ts.map +1 -1
- package/build-types/utils/test/block-selection-history.test.d.ts +2 -0
- package/build-types/utils/test/block-selection-history.test.d.ts.map +1 -0
- package/build-types/utils/test/crdt-blocks.d.ts +2 -0
- package/build-types/utils/test/crdt-blocks.d.ts.map +1 -0
- package/build-types/utils/test/crdt.d.ts +2 -0
- package/build-types/utils/test/crdt.d.ts.map +1 -0
- package/package.json +21 -18
- package/src/actions.js +40 -7
- package/src/awareness/base-awareness.ts +50 -0
- package/src/awareness/config.ts +9 -0
- package/src/awareness/post-editor-awareness.ts +167 -0
- package/src/awareness/types.ts +38 -0
- package/src/awareness/utils.ts +159 -0
- package/src/entities.js +32 -2
- package/src/entity-types/test/attachment.test.ts +4 -4
- package/src/resolvers.js +53 -1
- package/src/test/actions.js +402 -0
- package/src/test/entity-provider.js +2 -0
- package/src/test/resolvers.js +4 -0
- package/src/types.ts +12 -3
- package/src/utils/block-selection-history.ts +176 -0
- package/src/utils/crdt-selection.ts +205 -0
- package/src/utils/crdt-user-selections.ts +336 -0
- package/src/utils/crdt-utils.ts +54 -0
- package/src/utils/crdt.ts +36 -3
- package/src/utils/test/block-selection-history.test.ts +764 -0
- package/src/utils/test/crdt-blocks.ts +11 -4
|
@@ -0,0 +1,764 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External dependencies
|
|
3
|
+
*/
|
|
4
|
+
import { Y } from '@wordpress/sync';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Internal dependencies
|
|
8
|
+
*/
|
|
9
|
+
import {
|
|
10
|
+
createBlockSelectionHistory,
|
|
11
|
+
YSelectionType,
|
|
12
|
+
type YRelativeSelection,
|
|
13
|
+
type BlockSelectionHistory,
|
|
14
|
+
} from '../block-selection-history';
|
|
15
|
+
import { CRDT_RECORD_MAP_KEY } from '../../sync';
|
|
16
|
+
import type { WPSelection } from '../../types';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Helper function to create a simple Y.Doc with blocks for testing
|
|
20
|
+
*/
|
|
21
|
+
function createTestDoc() {
|
|
22
|
+
const ydoc = new Y.Doc();
|
|
23
|
+
const documentMap = ydoc.getMap( CRDT_RECORD_MAP_KEY );
|
|
24
|
+
const blocks = new Y.Array();
|
|
25
|
+
documentMap.set( 'blocks', blocks );
|
|
26
|
+
|
|
27
|
+
// Create block 1 with a content attribute
|
|
28
|
+
const block1 = new Y.Map();
|
|
29
|
+
block1.set( 'clientId', 'block-1' );
|
|
30
|
+
const block1Attrs = new Y.Map();
|
|
31
|
+
block1Attrs.set( 'content', new Y.Text( 'Hello world' ) );
|
|
32
|
+
block1.set( 'attributes', block1Attrs );
|
|
33
|
+
block1.set( 'innerBlocks', new Y.Array() );
|
|
34
|
+
blocks.push( [ block1 ] );
|
|
35
|
+
|
|
36
|
+
// Create block 2 with a content attribute
|
|
37
|
+
const block2 = new Y.Map();
|
|
38
|
+
block2.set( 'clientId', 'block-2' );
|
|
39
|
+
const block2Attrs = new Y.Map();
|
|
40
|
+
block2Attrs.set( 'content', new Y.Text( 'Second block' ) );
|
|
41
|
+
block2.set( 'attributes', block2Attrs );
|
|
42
|
+
block2.set( 'innerBlocks', new Y.Array() );
|
|
43
|
+
blocks.push( [ block2 ] );
|
|
44
|
+
|
|
45
|
+
// Create block 3 with a different attribute key
|
|
46
|
+
const block3 = new Y.Map();
|
|
47
|
+
block3.set( 'clientId', 'block-3' );
|
|
48
|
+
const block3Attrs = new Y.Map();
|
|
49
|
+
block3Attrs.set( 'value', new Y.Text( 'Third block with value attr' ) );
|
|
50
|
+
block3.set( 'attributes', block3Attrs );
|
|
51
|
+
block3.set( 'innerBlocks', new Y.Array() );
|
|
52
|
+
blocks.push( [ block3 ] );
|
|
53
|
+
|
|
54
|
+
return ydoc;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function createSelection(
|
|
58
|
+
start: { clientId: string; attributeKey?: string; offset?: number },
|
|
59
|
+
end?: { clientId: string; attributeKey?: string; offset?: number }
|
|
60
|
+
): WPSelection {
|
|
61
|
+
const selectionStart = {
|
|
62
|
+
clientId: start.clientId,
|
|
63
|
+
attributeKey: start.attributeKey as string,
|
|
64
|
+
offset: start.offset ?? 0,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const selectionEnd = end
|
|
68
|
+
? {
|
|
69
|
+
clientId: end.clientId,
|
|
70
|
+
attributeKey: end.attributeKey as string,
|
|
71
|
+
offset: end.offset ?? 0,
|
|
72
|
+
}
|
|
73
|
+
: selectionStart;
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
selectionStart,
|
|
77
|
+
selectionEnd,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
describe( 'BlockSelectionHistory', () => {
|
|
82
|
+
let history: BlockSelectionHistory;
|
|
83
|
+
let ydoc: Y.Doc;
|
|
84
|
+
|
|
85
|
+
beforeEach( () => {
|
|
86
|
+
ydoc = createTestDoc();
|
|
87
|
+
history = createBlockSelectionHistory( ydoc, 5 );
|
|
88
|
+
} );
|
|
89
|
+
|
|
90
|
+
afterEach( () => {
|
|
91
|
+
jest.restoreAllMocks();
|
|
92
|
+
} );
|
|
93
|
+
|
|
94
|
+
describe( 'initialization', () => {
|
|
95
|
+
test( 'should initialize with empty history', () => {
|
|
96
|
+
expect( history.getSelectionHistory() ).toEqual( [] );
|
|
97
|
+
} );
|
|
98
|
+
} );
|
|
99
|
+
|
|
100
|
+
describe( 'updateSelection with relative positions', () => {
|
|
101
|
+
test( 'should convert and store a selection as a relative position', () => {
|
|
102
|
+
const selection = createSelection( {
|
|
103
|
+
clientId: 'block-1',
|
|
104
|
+
attributeKey: 'content',
|
|
105
|
+
offset: 5,
|
|
106
|
+
} );
|
|
107
|
+
history.updateSelection( selection );
|
|
108
|
+
|
|
109
|
+
const selectionHistory = history.getSelectionHistory();
|
|
110
|
+
expect( selectionHistory.length ).toBe( 1 );
|
|
111
|
+
|
|
112
|
+
const fullSelection = selectionHistory[ 0 ];
|
|
113
|
+
expect( fullSelection.start.type ).toBe(
|
|
114
|
+
YSelectionType.RelativeSelection
|
|
115
|
+
);
|
|
116
|
+
expect( fullSelection.end.type ).toBe(
|
|
117
|
+
YSelectionType.RelativeSelection
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const startPosition = fullSelection.start as YRelativeSelection;
|
|
121
|
+
const endPosition = fullSelection.end as YRelativeSelection;
|
|
122
|
+
|
|
123
|
+
expect( startPosition.clientId ).toBe( 'block-1' );
|
|
124
|
+
expect( startPosition.attributeKey ).toBe( 'content' );
|
|
125
|
+
expect( startPosition.offset ).toBe( 5 );
|
|
126
|
+
expect( startPosition.relativePosition ).toBeDefined();
|
|
127
|
+
|
|
128
|
+
expect( endPosition.clientId ).toBe( 'block-1' );
|
|
129
|
+
expect( endPosition.attributeKey ).toBe( 'content' );
|
|
130
|
+
expect( endPosition.offset ).toBe( 5 );
|
|
131
|
+
expect( endPosition.relativePosition ).toBeDefined();
|
|
132
|
+
} );
|
|
133
|
+
|
|
134
|
+
test( 'should update position when selection changes within same block', () => {
|
|
135
|
+
const selection1 = createSelection( {
|
|
136
|
+
clientId: 'block-1',
|
|
137
|
+
attributeKey: 'content',
|
|
138
|
+
offset: 5,
|
|
139
|
+
} );
|
|
140
|
+
history.updateSelection( selection1 );
|
|
141
|
+
|
|
142
|
+
const selection2 = createSelection( {
|
|
143
|
+
clientId: 'block-1',
|
|
144
|
+
attributeKey: 'content',
|
|
145
|
+
offset: 8,
|
|
146
|
+
} );
|
|
147
|
+
history.updateSelection( selection2 );
|
|
148
|
+
|
|
149
|
+
const selectionHistory = history.getSelectionHistory();
|
|
150
|
+
|
|
151
|
+
// Should have only one selection in block history (still in same block)
|
|
152
|
+
expect( selectionHistory.length ).toBe( 1 );
|
|
153
|
+
|
|
154
|
+
const fullSelection = selectionHistory[ 0 ];
|
|
155
|
+
expect( fullSelection.start.clientId ).toBe( 'block-1' );
|
|
156
|
+
expect( fullSelection.start.type ).toBe(
|
|
157
|
+
YSelectionType.RelativeSelection
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const startPosition = fullSelection.start as YRelativeSelection;
|
|
161
|
+
expect( startPosition.offset ).toBe( 8 );
|
|
162
|
+
} );
|
|
163
|
+
|
|
164
|
+
test( 'should add new position when moving to different block', () => {
|
|
165
|
+
const selection1 = createSelection( {
|
|
166
|
+
clientId: 'block-1',
|
|
167
|
+
attributeKey: 'content',
|
|
168
|
+
offset: 5,
|
|
169
|
+
} );
|
|
170
|
+
history.updateSelection( selection1 );
|
|
171
|
+
|
|
172
|
+
const selection2 = createSelection( {
|
|
173
|
+
clientId: 'block-2',
|
|
174
|
+
attributeKey: 'content',
|
|
175
|
+
offset: 3,
|
|
176
|
+
} );
|
|
177
|
+
history.updateSelection( selection2 );
|
|
178
|
+
|
|
179
|
+
const selectionHistory = history.getSelectionHistory();
|
|
180
|
+
expect( selectionHistory.length ).toBe( 2 );
|
|
181
|
+
|
|
182
|
+
// Current selection at index 0
|
|
183
|
+
expect( selectionHistory[ 0 ].start.clientId ).toBe( 'block-2' );
|
|
184
|
+
expect( selectionHistory[ 0 ].end.clientId ).toBe( 'block-2' );
|
|
185
|
+
// Previous selection at index 1
|
|
186
|
+
expect( selectionHistory[ 1 ].start.clientId ).toBe( 'block-1' );
|
|
187
|
+
expect( selectionHistory[ 1 ].end.clientId ).toBe( 'block-1' );
|
|
188
|
+
} );
|
|
189
|
+
|
|
190
|
+
test( 'should store offset 0 when offset is not provided', () => {
|
|
191
|
+
const selection = createSelection( {
|
|
192
|
+
clientId: 'block-1',
|
|
193
|
+
attributeKey: 'content',
|
|
194
|
+
} );
|
|
195
|
+
history.updateSelection( selection );
|
|
196
|
+
|
|
197
|
+
const selectionHistory = history.getSelectionHistory();
|
|
198
|
+
expect( selectionHistory.length ).toBe( 1 );
|
|
199
|
+
|
|
200
|
+
const fullSelection = selectionHistory[ 0 ];
|
|
201
|
+
expect( fullSelection.start.type ).toBe(
|
|
202
|
+
YSelectionType.RelativeSelection
|
|
203
|
+
);
|
|
204
|
+
const startPosition = fullSelection.start as YRelativeSelection;
|
|
205
|
+
expect( startPosition.offset ).toBe( 0 );
|
|
206
|
+
|
|
207
|
+
expect( fullSelection.end.type ).toBe(
|
|
208
|
+
YSelectionType.RelativeSelection
|
|
209
|
+
);
|
|
210
|
+
const endPosition = fullSelection.end as YRelativeSelection;
|
|
211
|
+
expect( endPosition.offset ).toBe( 0 );
|
|
212
|
+
} );
|
|
213
|
+
} );
|
|
214
|
+
|
|
215
|
+
describe( 'updateSelection with block positions', () => {
|
|
216
|
+
test( 'should create block position (not relative) when clientId does not exist', () => {
|
|
217
|
+
const selection = createSelection( {
|
|
218
|
+
clientId: 'non-existent-block',
|
|
219
|
+
attributeKey: 'content',
|
|
220
|
+
offset: 5,
|
|
221
|
+
} );
|
|
222
|
+
history.updateSelection( selection );
|
|
223
|
+
|
|
224
|
+
const selectionHistory = history.getSelectionHistory();
|
|
225
|
+
expect( selectionHistory.length ).toBe( 1 );
|
|
226
|
+
|
|
227
|
+
const fullSelection = selectionHistory[ 0 ];
|
|
228
|
+
expect( fullSelection.start.type ).toBe(
|
|
229
|
+
YSelectionType.BlockSelection
|
|
230
|
+
);
|
|
231
|
+
expect( fullSelection.start.clientId ).toBe( 'non-existent-block' );
|
|
232
|
+
expect( fullSelection.end.type ).toBe(
|
|
233
|
+
YSelectionType.BlockSelection
|
|
234
|
+
);
|
|
235
|
+
expect( fullSelection.end.clientId ).toBe( 'non-existent-block' );
|
|
236
|
+
} );
|
|
237
|
+
|
|
238
|
+
test( 'should create block position (not relative) when attribute does not exist', () => {
|
|
239
|
+
const selection = createSelection( {
|
|
240
|
+
clientId: 'block-1',
|
|
241
|
+
attributeKey: 'nonexistent',
|
|
242
|
+
offset: 5,
|
|
243
|
+
} );
|
|
244
|
+
history.updateSelection( selection );
|
|
245
|
+
|
|
246
|
+
const selectionHistory = history.getSelectionHistory();
|
|
247
|
+
expect( selectionHistory.length ).toBe( 1 );
|
|
248
|
+
|
|
249
|
+
const fullSelection = selectionHistory[ 0 ];
|
|
250
|
+
expect( fullSelection.start.type ).toBe(
|
|
251
|
+
YSelectionType.BlockSelection
|
|
252
|
+
);
|
|
253
|
+
expect( fullSelection.start.clientId ).toBe( 'block-1' );
|
|
254
|
+
expect( fullSelection.end.type ).toBe(
|
|
255
|
+
YSelectionType.BlockSelection
|
|
256
|
+
);
|
|
257
|
+
expect( fullSelection.end.clientId ).toBe( 'block-1' );
|
|
258
|
+
} );
|
|
259
|
+
} );
|
|
260
|
+
|
|
261
|
+
describe( 'updateSelection edge cases', () => {
|
|
262
|
+
test( 'should ignore null selection', () => {
|
|
263
|
+
history.updateSelection( null as any );
|
|
264
|
+
expect( history.getSelectionHistory() ).toEqual( [] );
|
|
265
|
+
} );
|
|
266
|
+
|
|
267
|
+
test( 'should ignore undefined selection', () => {
|
|
268
|
+
history.updateSelection( undefined as any );
|
|
269
|
+
expect( history.getSelectionHistory() ).toEqual( [] );
|
|
270
|
+
} );
|
|
271
|
+
|
|
272
|
+
test( 'should ignore selection without clientId', () => {
|
|
273
|
+
const invalidSelection = {
|
|
274
|
+
selectionStart: {
|
|
275
|
+
clientId: '',
|
|
276
|
+
attributeKey: 'content',
|
|
277
|
+
offset: 0,
|
|
278
|
+
},
|
|
279
|
+
selectionEnd: {
|
|
280
|
+
clientId: '',
|
|
281
|
+
attributeKey: 'content',
|
|
282
|
+
offset: 0,
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
history.updateSelection( invalidSelection );
|
|
286
|
+
expect( history.getSelectionHistory() ).toEqual( [] );
|
|
287
|
+
} );
|
|
288
|
+
} );
|
|
289
|
+
|
|
290
|
+
describe( 'history management', () => {
|
|
291
|
+
test( 'should maintain history of last N unique blocks', () => {
|
|
292
|
+
const selections = [
|
|
293
|
+
createSelection( {
|
|
294
|
+
clientId: 'block-1',
|
|
295
|
+
attributeKey: 'content',
|
|
296
|
+
offset: 1,
|
|
297
|
+
} ),
|
|
298
|
+
createSelection( {
|
|
299
|
+
clientId: 'block-2',
|
|
300
|
+
attributeKey: 'content',
|
|
301
|
+
offset: 2,
|
|
302
|
+
} ),
|
|
303
|
+
createSelection( {
|
|
304
|
+
clientId: 'block-3',
|
|
305
|
+
attributeKey: 'value',
|
|
306
|
+
offset: 3,
|
|
307
|
+
} ),
|
|
308
|
+
];
|
|
309
|
+
|
|
310
|
+
selections.forEach( ( sel ) => history.updateSelection( sel ) );
|
|
311
|
+
|
|
312
|
+
const selectionHistory = history.getSelectionHistory();
|
|
313
|
+
expect( selectionHistory.length ).toBe( 3 );
|
|
314
|
+
|
|
315
|
+
// Current selection at index 0
|
|
316
|
+
expect( selectionHistory[ 0 ].start.clientId ).toBe( 'block-3' );
|
|
317
|
+
expect( selectionHistory[ 0 ].end.clientId ).toBe( 'block-3' );
|
|
318
|
+
// Previous selections
|
|
319
|
+
expect( selectionHistory[ 1 ].start.clientId ).toBe( 'block-2' );
|
|
320
|
+
expect( selectionHistory[ 1 ].end.clientId ).toBe( 'block-2' );
|
|
321
|
+
expect( selectionHistory[ 2 ].start.clientId ).toBe( 'block-1' );
|
|
322
|
+
expect( selectionHistory[ 2 ].end.clientId ).toBe( 'block-1' );
|
|
323
|
+
} );
|
|
324
|
+
|
|
325
|
+
test( 'should respect history size limit', () => {
|
|
326
|
+
const smallHistory = createBlockSelectionHistory( ydoc, 3 );
|
|
327
|
+
|
|
328
|
+
// Add more selections than history size
|
|
329
|
+
for ( let i = 1; i <= 5; i++ ) {
|
|
330
|
+
const selection = createSelection( {
|
|
331
|
+
clientId: `block-${ i }`,
|
|
332
|
+
attributeKey: 'content',
|
|
333
|
+
offset: i,
|
|
334
|
+
} );
|
|
335
|
+
smallHistory.updateSelection( selection );
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Should only keep last 4 total (3 history + 1 current)
|
|
339
|
+
const selectionHistory = smallHistory.getSelectionHistory();
|
|
340
|
+
expect( selectionHistory.length ).toBe( 4 );
|
|
341
|
+
|
|
342
|
+
// Current selection at index 0
|
|
343
|
+
expect( selectionHistory[ 0 ].start.clientId ).toBe( 'block-5' );
|
|
344
|
+
expect( selectionHistory[ 0 ].end.clientId ).toBe( 'block-5' );
|
|
345
|
+
// Previous 3 selections
|
|
346
|
+
expect( selectionHistory[ 1 ].start.clientId ).toBe( 'block-4' );
|
|
347
|
+
expect( selectionHistory[ 2 ].start.clientId ).toBe( 'block-3' );
|
|
348
|
+
expect( selectionHistory[ 3 ].start.clientId ).toBe( 'block-2' );
|
|
349
|
+
} );
|
|
350
|
+
|
|
351
|
+
test( 'should remove duplicate block from history when revisited', () => {
|
|
352
|
+
history.updateSelection(
|
|
353
|
+
createSelection( {
|
|
354
|
+
clientId: 'block-1',
|
|
355
|
+
attributeKey: 'content',
|
|
356
|
+
offset: 1,
|
|
357
|
+
} )
|
|
358
|
+
);
|
|
359
|
+
history.updateSelection(
|
|
360
|
+
createSelection( {
|
|
361
|
+
clientId: 'block-2',
|
|
362
|
+
attributeKey: 'content',
|
|
363
|
+
offset: 2,
|
|
364
|
+
} )
|
|
365
|
+
);
|
|
366
|
+
history.updateSelection(
|
|
367
|
+
createSelection( {
|
|
368
|
+
clientId: 'block-3',
|
|
369
|
+
attributeKey: 'value',
|
|
370
|
+
offset: 3,
|
|
371
|
+
} )
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
// Go back to block-1
|
|
375
|
+
history.updateSelection(
|
|
376
|
+
createSelection( {
|
|
377
|
+
clientId: 'block-1',
|
|
378
|
+
attributeKey: 'content',
|
|
379
|
+
offset: 5,
|
|
380
|
+
} )
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
const selectionHistory = history.getSelectionHistory();
|
|
384
|
+
|
|
385
|
+
// block-1 should be current (most recent) at index 0
|
|
386
|
+
expect( selectionHistory[ 0 ].start.clientId ).toBe( 'block-1' );
|
|
387
|
+
expect( selectionHistory[ 0 ].start.type ).toBe(
|
|
388
|
+
YSelectionType.RelativeSelection
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
const startPosition = selectionHistory[ 0 ]
|
|
392
|
+
.start as YRelativeSelection;
|
|
393
|
+
expect( startPosition.offset ).toBe( 5 ); // Updated offset
|
|
394
|
+
|
|
395
|
+
// block-1 should not appear again in history
|
|
396
|
+
expect( selectionHistory.length ).toBe( 3 );
|
|
397
|
+
expect( selectionHistory[ 1 ].start.clientId ).toBe( 'block-3' );
|
|
398
|
+
expect( selectionHistory[ 2 ].start.clientId ).toBe( 'block-2' );
|
|
399
|
+
} );
|
|
400
|
+
} );
|
|
401
|
+
|
|
402
|
+
describe( 'getSelectionHistory', () => {
|
|
403
|
+
test( 'should return current selection when only one selection exists', () => {
|
|
404
|
+
history.updateSelection(
|
|
405
|
+
createSelection( {
|
|
406
|
+
clientId: 'block-1',
|
|
407
|
+
attributeKey: 'content',
|
|
408
|
+
offset: 1,
|
|
409
|
+
} )
|
|
410
|
+
);
|
|
411
|
+
const selectionHistory = history.getSelectionHistory();
|
|
412
|
+
expect( selectionHistory.length ).toBe( 1 );
|
|
413
|
+
expect( selectionHistory[ 0 ].start.clientId ).toBe( 'block-1' );
|
|
414
|
+
} );
|
|
415
|
+
|
|
416
|
+
test( 'should return most recent position at index 0', () => {
|
|
417
|
+
history.updateSelection(
|
|
418
|
+
createSelection( {
|
|
419
|
+
clientId: 'block-1',
|
|
420
|
+
attributeKey: 'content',
|
|
421
|
+
offset: 1,
|
|
422
|
+
} )
|
|
423
|
+
);
|
|
424
|
+
history.updateSelection(
|
|
425
|
+
createSelection( {
|
|
426
|
+
clientId: 'block-2',
|
|
427
|
+
attributeKey: 'content',
|
|
428
|
+
offset: 2,
|
|
429
|
+
} )
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
const selectionHistory = history.getSelectionHistory();
|
|
433
|
+
expect( selectionHistory.length ).toBe( 2 );
|
|
434
|
+
expect( selectionHistory[ 0 ].start.clientId ).toBe( 'block-2' );
|
|
435
|
+
expect( selectionHistory[ 0 ].end.clientId ).toBe( 'block-2' );
|
|
436
|
+
} );
|
|
437
|
+
|
|
438
|
+
test( 'should return empty array when history is empty', () => {
|
|
439
|
+
expect( history.getSelectionHistory() ).toEqual( [] );
|
|
440
|
+
} );
|
|
441
|
+
} );
|
|
442
|
+
|
|
443
|
+
describe( 'relative position accuracy', () => {
|
|
444
|
+
test( 'should create relative position that survives text insertion before it', () => {
|
|
445
|
+
const selection = createSelection( {
|
|
446
|
+
clientId: 'block-1',
|
|
447
|
+
attributeKey: 'content',
|
|
448
|
+
offset: 5,
|
|
449
|
+
} );
|
|
450
|
+
history.updateSelection( selection );
|
|
451
|
+
|
|
452
|
+
const selectionHistory = history.getSelectionHistory();
|
|
453
|
+
expect( selectionHistory.length ).toBe( 1 );
|
|
454
|
+
|
|
455
|
+
const fullSelection = selectionHistory[ 0 ];
|
|
456
|
+
expect( fullSelection.start.type ).toBe(
|
|
457
|
+
YSelectionType.RelativeSelection
|
|
458
|
+
);
|
|
459
|
+
const startPosition = fullSelection.start as YRelativeSelection;
|
|
460
|
+
// Get the Y.Text and insert text before the position
|
|
461
|
+
const documentMap = ydoc.getMap( CRDT_RECORD_MAP_KEY );
|
|
462
|
+
const blocks = documentMap.get( 'blocks' ) as Y.Array< any >;
|
|
463
|
+
const block1 = blocks.get( 0 ) as Y.Map< any >;
|
|
464
|
+
const attrs = block1.get( 'attributes' ) as Y.Map< Y.Text >;
|
|
465
|
+
const ytext = attrs.get( 'content' );
|
|
466
|
+
if ( ! ytext ) {
|
|
467
|
+
throw new Error( 'Y.Text not found' );
|
|
468
|
+
}
|
|
469
|
+
ytext.insert( 0, 'PREFIX ' ); // Insert at beginning
|
|
470
|
+
|
|
471
|
+
// Convert relative position back to absolute
|
|
472
|
+
const absolutePosition =
|
|
473
|
+
Y.createAbsolutePositionFromRelativePosition(
|
|
474
|
+
startPosition.relativePosition,
|
|
475
|
+
ydoc
|
|
476
|
+
);
|
|
477
|
+
|
|
478
|
+
// The absolute index should have shifted by 7 (length of 'PREFIX ')
|
|
479
|
+
expect( absolutePosition?.index ).toBe( 12 ); // 5 + 7
|
|
480
|
+
} );
|
|
481
|
+
|
|
482
|
+
test( 'should create relative position that survives text deletion before it', () => {
|
|
483
|
+
const selection = createSelection( {
|
|
484
|
+
clientId: 'block-1',
|
|
485
|
+
attributeKey: 'content',
|
|
486
|
+
offset: 8,
|
|
487
|
+
} );
|
|
488
|
+
history.updateSelection( selection );
|
|
489
|
+
|
|
490
|
+
const selectionHistory = history.getSelectionHistory();
|
|
491
|
+
expect( selectionHistory.length ).toBe( 1 );
|
|
492
|
+
|
|
493
|
+
const fullSelection = selectionHistory[ 0 ];
|
|
494
|
+
expect( fullSelection.start.type ).toBe(
|
|
495
|
+
YSelectionType.RelativeSelection
|
|
496
|
+
);
|
|
497
|
+
const startPosition = fullSelection.start as YRelativeSelection;
|
|
498
|
+
// Delete text before the position
|
|
499
|
+
const documentMap = ydoc.getMap( CRDT_RECORD_MAP_KEY );
|
|
500
|
+
const blocks = documentMap.get( 'blocks' ) as Y.Array< any >;
|
|
501
|
+
const block1 = blocks.get( 0 ) as Y.Map< any >;
|
|
502
|
+
const attrs = block1.get( 'attributes' ) as Y.Map< Y.Text >;
|
|
503
|
+
const ytext = attrs.get( 'content' );
|
|
504
|
+
if ( ! ytext ) {
|
|
505
|
+
throw new Error( 'Y.Text not found' );
|
|
506
|
+
}
|
|
507
|
+
ytext.delete( 0, 5 ); // Delete "Hello"
|
|
508
|
+
|
|
509
|
+
// Convert relative position back to absolute
|
|
510
|
+
const absolutePosition =
|
|
511
|
+
Y.createAbsolutePositionFromRelativePosition(
|
|
512
|
+
startPosition.relativePosition,
|
|
513
|
+
ydoc
|
|
514
|
+
);
|
|
515
|
+
|
|
516
|
+
// The absolute index should have shifted back by 5
|
|
517
|
+
expect( absolutePosition?.index ).toBe( 3 ); // 8 - 5
|
|
518
|
+
} );
|
|
519
|
+
} );
|
|
520
|
+
|
|
521
|
+
describe( 'integration scenarios', () => {
|
|
522
|
+
test( 'should handle rapid selection changes in same block', () => {
|
|
523
|
+
for ( let i = 0; i < 10; i++ ) {
|
|
524
|
+
history.updateSelection(
|
|
525
|
+
createSelection( {
|
|
526
|
+
clientId: 'block-1',
|
|
527
|
+
attributeKey: 'content',
|
|
528
|
+
offset: i,
|
|
529
|
+
} )
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const selectionHistory = history.getSelectionHistory();
|
|
534
|
+
|
|
535
|
+
// Should have only one selection in block history (still in same block)
|
|
536
|
+
expect( selectionHistory.length ).toBe( 1 );
|
|
537
|
+
|
|
538
|
+
const current = selectionHistory[ 0 ];
|
|
539
|
+
expect( current.start.clientId ).toBe( 'block-1' );
|
|
540
|
+
expect( current.start.type ).toBe(
|
|
541
|
+
YSelectionType.RelativeSelection
|
|
542
|
+
);
|
|
543
|
+
const startPosition = current.start as YRelativeSelection;
|
|
544
|
+
|
|
545
|
+
expect( startPosition.offset ).toBe( 9 );
|
|
546
|
+
} );
|
|
547
|
+
|
|
548
|
+
test( 'should handle alternating between two blocks', () => {
|
|
549
|
+
history.updateSelection(
|
|
550
|
+
createSelection( {
|
|
551
|
+
clientId: 'block-1',
|
|
552
|
+
attributeKey: 'content',
|
|
553
|
+
offset: 1,
|
|
554
|
+
} )
|
|
555
|
+
);
|
|
556
|
+
history.updateSelection(
|
|
557
|
+
createSelection( {
|
|
558
|
+
clientId: 'block-2',
|
|
559
|
+
attributeKey: 'content',
|
|
560
|
+
offset: 2,
|
|
561
|
+
} )
|
|
562
|
+
);
|
|
563
|
+
history.updateSelection(
|
|
564
|
+
createSelection( {
|
|
565
|
+
clientId: 'block-1',
|
|
566
|
+
attributeKey: 'content',
|
|
567
|
+
offset: 3,
|
|
568
|
+
} )
|
|
569
|
+
);
|
|
570
|
+
history.updateSelection(
|
|
571
|
+
createSelection( {
|
|
572
|
+
clientId: 'block-2',
|
|
573
|
+
attributeKey: 'content',
|
|
574
|
+
offset: 4,
|
|
575
|
+
} )
|
|
576
|
+
);
|
|
577
|
+
|
|
578
|
+
const selectionHistory = history.getSelectionHistory();
|
|
579
|
+
|
|
580
|
+
// Current selection at index 0
|
|
581
|
+
expect( selectionHistory[ 0 ].start.clientId ).toBe( 'block-2' );
|
|
582
|
+
expect( selectionHistory[ 0 ].end.clientId ).toBe( 'block-2' );
|
|
583
|
+
// Only one previous selection (block-1)
|
|
584
|
+
expect( selectionHistory[ 1 ].start.clientId ).toBe( 'block-1' );
|
|
585
|
+
|
|
586
|
+
// Should only have 2 entries total (current + 1 previous)
|
|
587
|
+
expect( selectionHistory.length ).toBe( 2 );
|
|
588
|
+
} );
|
|
589
|
+
|
|
590
|
+
test( 'should handle mixed block and relative selections', () => {
|
|
591
|
+
history.updateSelection(
|
|
592
|
+
createSelection( {
|
|
593
|
+
clientId: 'block-1',
|
|
594
|
+
attributeKey: 'content',
|
|
595
|
+
offset: 5,
|
|
596
|
+
} )
|
|
597
|
+
);
|
|
598
|
+
history.updateSelection(
|
|
599
|
+
createSelection( {
|
|
600
|
+
clientId: 'non-existent',
|
|
601
|
+
attributeKey: 'content',
|
|
602
|
+
offset: 0,
|
|
603
|
+
} )
|
|
604
|
+
);
|
|
605
|
+
history.updateSelection(
|
|
606
|
+
createSelection( {
|
|
607
|
+
clientId: 'block-2',
|
|
608
|
+
attributeKey: 'content',
|
|
609
|
+
offset: 3,
|
|
610
|
+
} )
|
|
611
|
+
);
|
|
612
|
+
|
|
613
|
+
const selectionHistory = history.getSelectionHistory();
|
|
614
|
+
const current = selectionHistory[ 0 ];
|
|
615
|
+
const backupSelections = selectionHistory.slice( 1 );
|
|
616
|
+
|
|
617
|
+
expect( current?.start.type ).toBe(
|
|
618
|
+
YSelectionType.RelativeSelection
|
|
619
|
+
);
|
|
620
|
+
expect( backupSelections[ 0 ]?.start.type ).toBe(
|
|
621
|
+
YSelectionType.BlockSelection
|
|
622
|
+
);
|
|
623
|
+
expect( backupSelections[ 1 ]?.start.type ).toBe(
|
|
624
|
+
YSelectionType.RelativeSelection
|
|
625
|
+
);
|
|
626
|
+
} );
|
|
627
|
+
} );
|
|
628
|
+
|
|
629
|
+
describe( 'cross-block selections', () => {
|
|
630
|
+
test( 'should update currentSelection when both start and end are within same block', () => {
|
|
631
|
+
const selection1 = createSelection(
|
|
632
|
+
{ clientId: 'block-1', attributeKey: 'content', offset: 2 },
|
|
633
|
+
{ clientId: 'block-1', attributeKey: 'content', offset: 8 }
|
|
634
|
+
);
|
|
635
|
+
history.updateSelection( selection1 );
|
|
636
|
+
|
|
637
|
+
const selection2 = createSelection(
|
|
638
|
+
{ clientId: 'block-1', attributeKey: 'content', offset: 2 },
|
|
639
|
+
{ clientId: 'block-1', attributeKey: 'content', offset: 10 }
|
|
640
|
+
);
|
|
641
|
+
history.updateSelection( selection2 );
|
|
642
|
+
|
|
643
|
+
const selectionHistory = history.getSelectionHistory();
|
|
644
|
+
const current = selectionHistory[ 0 ];
|
|
645
|
+
|
|
646
|
+
// Both selections are in same block, so should update current without adding to history
|
|
647
|
+
expect( current?.start.clientId ).toBe( 'block-1' );
|
|
648
|
+
expect( current?.end.clientId ).toBe( 'block-1' );
|
|
649
|
+
expect( ( current?.start as YRelativeSelection ).offset ).toBe( 2 );
|
|
650
|
+
expect( ( current?.end as YRelativeSelection ).offset ).toBe( 10 );
|
|
651
|
+
expect( selectionHistory.length ).toBe( 1 );
|
|
652
|
+
} );
|
|
653
|
+
|
|
654
|
+
test( 'should track cross-block selection spanning two blocks', () => {
|
|
655
|
+
const selection = createSelection(
|
|
656
|
+
{ clientId: 'block-1', attributeKey: 'content', offset: 5 },
|
|
657
|
+
{ clientId: 'block-2', attributeKey: 'content', offset: 3 }
|
|
658
|
+
);
|
|
659
|
+
history.updateSelection( selection );
|
|
660
|
+
|
|
661
|
+
const selectionHistory = history.getSelectionHistory();
|
|
662
|
+
const current = selectionHistory[ 0 ];
|
|
663
|
+
|
|
664
|
+
expect( current?.start.clientId ).toBe( 'block-1' );
|
|
665
|
+
expect( current?.start.type ).toBe(
|
|
666
|
+
YSelectionType.RelativeSelection
|
|
667
|
+
);
|
|
668
|
+
expect( ( current?.start as YRelativeSelection ).offset ).toBe( 5 );
|
|
669
|
+
|
|
670
|
+
expect( current?.end.clientId ).toBe( 'block-2' );
|
|
671
|
+
expect( current?.end.type ).toBe(
|
|
672
|
+
YSelectionType.RelativeSelection
|
|
673
|
+
);
|
|
674
|
+
expect( ( current?.end as YRelativeSelection ).offset ).toBe( 3 );
|
|
675
|
+
} );
|
|
676
|
+
|
|
677
|
+
test( 'should differentiate between same-block and cross-block selections', () => {
|
|
678
|
+
// Single block selection
|
|
679
|
+
history.updateSelection(
|
|
680
|
+
createSelection( {
|
|
681
|
+
clientId: 'block-1',
|
|
682
|
+
attributeKey: 'content',
|
|
683
|
+
offset: 5,
|
|
684
|
+
} )
|
|
685
|
+
);
|
|
686
|
+
|
|
687
|
+
// Cross-block selection from block-1 to block-2
|
|
688
|
+
history.updateSelection(
|
|
689
|
+
createSelection(
|
|
690
|
+
{ clientId: 'block-1', attributeKey: 'content', offset: 3 },
|
|
691
|
+
{ clientId: 'block-2', attributeKey: 'content', offset: 2 }
|
|
692
|
+
)
|
|
693
|
+
);
|
|
694
|
+
|
|
695
|
+
const selectionHistory = history.getSelectionHistory();
|
|
696
|
+
const current = selectionHistory[ 0 ];
|
|
697
|
+
const blockHistory = selectionHistory.slice( 1 );
|
|
698
|
+
|
|
699
|
+
// Should have moved to different block combination
|
|
700
|
+
expect( current?.start.clientId ).toBe( 'block-1' );
|
|
701
|
+
expect( current?.end.clientId ).toBe( 'block-2' );
|
|
702
|
+
expect( blockHistory.length ).toBe( 1 );
|
|
703
|
+
expect( blockHistory[ 0 ].start.clientId ).toBe( 'block-1' );
|
|
704
|
+
expect( blockHistory[ 0 ].end.clientId ).toBe( 'block-1' );
|
|
705
|
+
} );
|
|
706
|
+
|
|
707
|
+
test( 'should add to history when transitioning between different cross-block selections', () => {
|
|
708
|
+
// Cross-block from block-1 to block-2
|
|
709
|
+
history.updateSelection(
|
|
710
|
+
createSelection(
|
|
711
|
+
{ clientId: 'block-1', attributeKey: 'content', offset: 5 },
|
|
712
|
+
{ clientId: 'block-2', attributeKey: 'content', offset: 3 }
|
|
713
|
+
)
|
|
714
|
+
);
|
|
715
|
+
|
|
716
|
+
// Cross-block from block-2 to block-3
|
|
717
|
+
history.updateSelection(
|
|
718
|
+
createSelection(
|
|
719
|
+
{ clientId: 'block-2', attributeKey: 'content', offset: 1 },
|
|
720
|
+
{ clientId: 'block-3', attributeKey: 'value', offset: 2 }
|
|
721
|
+
)
|
|
722
|
+
);
|
|
723
|
+
|
|
724
|
+
const selectionHistory = history.getSelectionHistory();
|
|
725
|
+
const current = selectionHistory[ 0 ];
|
|
726
|
+
const blockHistory = selectionHistory.slice( 1 );
|
|
727
|
+
|
|
728
|
+
expect( current?.start.clientId ).toBe( 'block-2' );
|
|
729
|
+
expect( current?.end.clientId ).toBe( 'block-3' );
|
|
730
|
+
expect( blockHistory.length ).toBe( 1 );
|
|
731
|
+
expect( blockHistory[ 0 ].start.clientId ).toBe( 'block-1' );
|
|
732
|
+
expect( blockHistory[ 0 ].end.clientId ).toBe( 'block-2' );
|
|
733
|
+
} );
|
|
734
|
+
|
|
735
|
+
test( 'should not add to history when updating same cross-block selection combination', () => {
|
|
736
|
+
// Cross-block from block-1 to block-2
|
|
737
|
+
history.updateSelection(
|
|
738
|
+
createSelection(
|
|
739
|
+
{ clientId: 'block-1', attributeKey: 'content', offset: 5 },
|
|
740
|
+
{ clientId: 'block-2', attributeKey: 'content', offset: 3 }
|
|
741
|
+
)
|
|
742
|
+
);
|
|
743
|
+
|
|
744
|
+
// Same block combination, different offsets
|
|
745
|
+
history.updateSelection(
|
|
746
|
+
createSelection(
|
|
747
|
+
{ clientId: 'block-1', attributeKey: 'content', offset: 2 },
|
|
748
|
+
{ clientId: 'block-2', attributeKey: 'content', offset: 8 }
|
|
749
|
+
)
|
|
750
|
+
);
|
|
751
|
+
|
|
752
|
+
const selectionHistory = history.getSelectionHistory();
|
|
753
|
+
const current = selectionHistory[ 0 ];
|
|
754
|
+
const blockHistory = selectionHistory.slice( 1 );
|
|
755
|
+
|
|
756
|
+
expect( current?.start.clientId ).toBe( 'block-1' );
|
|
757
|
+
expect( current?.end.clientId ).toBe( 'block-2' );
|
|
758
|
+
expect( ( current?.start as YRelativeSelection ).offset ).toBe( 2 );
|
|
759
|
+
expect( ( current?.end as YRelativeSelection ).offset ).toBe( 8 );
|
|
760
|
+
// Should not add to history - same block combination
|
|
761
|
+
expect( blockHistory.length ).toBe( 0 );
|
|
762
|
+
} );
|
|
763
|
+
} );
|
|
764
|
+
} );
|