0
# Position Tracking
1
2
Position system that maintains references across concurrent edits and structural changes. Yjs provides both relative and absolute position types that survive concurrent modifications from multiple users.
3
4
## Capabilities
5
6
### RelativePosition
7
8
Position that remains valid across concurrent document changes by referencing document structure.
9
10
```typescript { .api }
11
/**
12
* Position that remains stable across concurrent modifications
13
*/
14
class RelativePosition {
15
/** Type ID reference or null */
16
readonly type: ID | null;
17
18
/** Type name reference or null */
19
readonly tname: string | null;
20
21
/** Item ID reference or null */
22
readonly item: ID | null;
23
24
/** Association direction (-1, 0, or 1) */
25
readonly assoc: number;
26
}
27
```
28
29
### AbsolutePosition
30
31
Position resolved to a specific index within a type at a given document state.
32
33
```typescript { .api }
34
/**
35
* Position resolved to specific index within a type
36
*/
37
class AbsolutePosition {
38
/** The type containing this position */
39
readonly type: AbstractType<any>;
40
41
/** Index within the type */
42
readonly index: number;
43
44
/** Association direction (-1, 0, or 1) */
45
readonly assoc: number;
46
}
47
```
48
49
### Creating Relative Positions
50
51
Functions for creating relative positions from types and indices.
52
53
```typescript { .api }
54
/**
55
* Create relative position from type and index
56
* @param type - Type to create position in
57
* @param index - Index within the type
58
* @param assoc - Association direction (default: 0)
59
* @returns RelativePosition that tracks this location
60
*/
61
function createRelativePositionFromTypeIndex(
62
type: AbstractType<any>,
63
index: number,
64
assoc?: number
65
): RelativePosition;
66
67
/**
68
* Create relative position from JSON representation
69
* @param json - JSON object representing position
70
* @returns RelativePosition instance
71
*/
72
function createRelativePositionFromJSON(json: any): RelativePosition;
73
```
74
75
**Usage Examples:**
76
77
```typescript
78
import * as Y from "yjs";
79
80
const doc1 = new Y.Doc();
81
const ytext1 = doc1.getText("document");
82
ytext1.insert(0, "Hello World!");
83
84
// Create relative position at index 6 (before "World")
85
const relPos = Y.createRelativePositionFromTypeIndex(ytext1, 6);
86
87
// Position remains valid after other users make changes
88
const doc2 = new Y.Doc();
89
const ytext2 = doc2.getText("document");
90
91
// Simulate receiving updates from another user
92
const update1 = Y.encodeStateAsUpdate(doc1);
93
Y.applyUpdate(doc2, update1);
94
95
// Other user inserts text at beginning
96
ytext2.insert(0, "Hi! ");
97
98
// Create update and apply back to doc1
99
const update2 = Y.encodeStateAsUpdate(doc2);
100
Y.applyUpdate(doc1, update2);
101
102
// Original position still points to correct location
103
const absPos = Y.createAbsolutePositionFromRelativePosition(relPos, doc1);
104
console.log("Position now at index:", absPos?.index); // Adjusted index
105
```
106
107
### Converting Between Position Types
108
109
Functions for converting between relative and absolute positions.
110
111
```typescript { .api }
112
/**
113
* Convert relative position to absolute position
114
* @param rpos - Relative position to convert
115
* @param doc - Document to resolve position in
116
* @returns AbsolutePosition or null if position cannot be resolved
117
*/
118
function createAbsolutePositionFromRelativePosition(
119
rpos: RelativePosition,
120
doc: Doc
121
): AbsolutePosition | null;
122
123
/**
124
* Compare two relative positions for equality
125
* @param a - First relative position
126
* @param b - Second relative position
127
* @returns True if positions are equal
128
*/
129
function compareRelativePositions(a: RelativePosition | null, b: RelativePosition | null): boolean;
130
```
131
132
**Usage Examples:**
133
134
```typescript
135
import * as Y from "yjs";
136
137
const doc = new Y.Doc();
138
const ytext = doc.getText("document");
139
ytext.insert(0, "Hello World!");
140
141
// Create multiple relative positions
142
const pos1 = Y.createRelativePositionFromTypeIndex(ytext, 0); // Start
143
const pos2 = Y.createRelativePositionFromTypeIndex(ytext, 6); // Before "World"
144
const pos3 = Y.createRelativePositionFromTypeIndex(ytext, ytext.length); // End
145
146
// Convert to absolute positions
147
const abs1 = Y.createAbsolutePositionFromRelativePosition(pos1, doc);
148
const abs2 = Y.createAbsolutePositionFromRelativePosition(pos2, doc);
149
const abs3 = Y.createAbsolutePositionFromRelativePosition(pos3, doc);
150
151
console.log("Start position:", abs1?.index); // 0
152
console.log("Middle position:", abs2?.index); // 6
153
console.log("End position:", abs3?.index); // 12
154
155
// Compare positions
156
console.log("pos1 equals pos2:", Y.compareRelativePositions(pos1, pos2)); // false
157
console.log("pos1 equals pos1:", Y.compareRelativePositions(pos1, pos1)); // true
158
```
159
160
### Position Serialization
161
162
Functions for serializing and deserializing relative positions.
163
164
```typescript { .api }
165
/**
166
* Encode relative position to binary format
167
* @param rpos - Relative position to encode
168
* @returns Binary representation as Uint8Array
169
*/
170
function encodeRelativePosition(rpos: RelativePosition): Uint8Array;
171
172
/**
173
* Decode relative position from binary format
174
* @param uint8Array - Binary data to decode
175
* @returns RelativePosition instance
176
*/
177
function decodeRelativePosition(uint8Array: Uint8Array): RelativePosition;
178
179
/**
180
* Convert relative position to JSON format
181
* @param rpos - Relative position to convert
182
* @returns JSON representation
183
*/
184
function relativePositionToJSON(rpos: RelativePosition): any;
185
```
186
187
**Usage Examples:**
188
189
```typescript
190
import * as Y from "yjs";
191
192
const doc = new Y.Doc();
193
const ytext = doc.getText("document");
194
ytext.insert(0, "Hello World!");
195
196
const relPos = Y.createRelativePositionFromTypeIndex(ytext, 6);
197
198
// Serialize to binary
199
const binary = Y.encodeRelativePosition(relPos);
200
console.log("Binary size:", binary.length);
201
202
// Deserialize from binary
203
const restoredPos = Y.decodeRelativePosition(binary);
204
205
// Serialize to JSON
206
const json = Y.relativePositionToJSON(relPos);
207
console.log("JSON:", json);
208
209
// Restore from JSON
210
const posFromJSON = Y.createRelativePositionFromJSON(json);
211
212
// All positions should be equivalent
213
console.log("Original equals restored:", Y.compareRelativePositions(relPos, restoredPos));
214
console.log("Original equals from JSON:", Y.compareRelativePositions(relPos, posFromJSON));
215
```
216
217
### Position Association
218
219
Association determines behavior when content is inserted exactly at the position.
220
221
```typescript { .api }
222
/**
223
* Association values:
224
* -1: Position moves left when content inserted at this location
225
* 0: Default behavior
226
* 1: Position moves right when content inserted at this location
227
*/
228
type PositionAssociation = -1 | 0 | 1;
229
```
230
231
**Usage Examples:**
232
233
```typescript
234
import * as Y from "yjs";
235
236
const doc = new Y.Doc();
237
const ytext = doc.getText("document");
238
ytext.insert(0, "Hello");
239
240
// Create positions with different associations at index 5
241
const leftAssoc = Y.createRelativePositionFromTypeIndex(ytext, 5, -1);
242
const defaultAssoc = Y.createRelativePositionFromTypeIndex(ytext, 5, 0);
243
const rightAssoc = Y.createRelativePositionFromTypeIndex(ytext, 5, 1);
244
245
// Insert text at position 5
246
ytext.insert(5, " World");
247
248
// Check where positions ended up
249
const leftAbs = Y.createAbsolutePositionFromRelativePosition(leftAssoc, doc);
250
const defaultAbs = Y.createAbsolutePositionFromRelativePosition(defaultAssoc, doc);
251
const rightAbs = Y.createAbsolutePositionFromRelativePosition(rightAssoc, doc);
252
253
console.log("Left association:", leftAbs?.index); // 5 (before inserted content)
254
console.log("Default association:", defaultAbs?.index); // 5 or 11 (implementation dependent)
255
console.log("Right association:", rightAbs?.index); // 11 (after inserted content)
256
```
257
258
### Advanced Position Tracking
259
260
**Cursor Tracking:**
261
262
```typescript
263
import * as Y from "yjs";
264
265
class CursorTracker {
266
private doc: Y.Doc;
267
private ytext: Y.Text;
268
private positions: Map<string, Y.RelativePosition>;
269
270
constructor(doc: Y.Doc, textName: string) {
271
this.doc = doc;
272
this.ytext = doc.getText(textName);
273
this.positions = new Map();
274
}
275
276
setCursor(userId: string, index: number) {
277
const relPos = Y.createRelativePositionFromTypeIndex(this.ytext, index);
278
this.positions.set(userId, relPos);
279
}
280
281
getCursor(userId: string): number | null {
282
const relPos = this.positions.get(userId);
283
if (!relPos) return null;
284
285
const absPos = Y.createAbsolutePositionFromRelativePosition(relPos, this.doc);
286
return absPos?.index ?? null;
287
}
288
289
getAllCursors(): Map<string, number> {
290
const cursors = new Map<string, number>();
291
292
this.positions.forEach((relPos, userId) => {
293
const absPos = Y.createAbsolutePositionFromRelativePosition(relPos, this.doc);
294
if (absPos) {
295
cursors.set(userId, absPos.index);
296
}
297
});
298
299
return cursors;
300
}
301
}
302
303
// Usage
304
const doc = new Y.Doc();
305
const tracker = new CursorTracker(doc, "document");
306
307
tracker.setCursor("alice", 10);
308
tracker.setCursor("bob", 20);
309
310
// Cursors automatically adjust as document changes
311
const ytext = doc.getText("document");
312
ytext.insert(0, "Prefix ");
313
314
console.log("Alice cursor:", tracker.getCursor("alice")); // Adjusted position
315
console.log("Bob cursor:", tracker.getCursor("bob")); // Adjusted position
316
```
317
318
**Selection Ranges:**
319
320
```typescript
321
import * as Y from "yjs";
322
323
interface SelectionRange {
324
start: Y.RelativePosition;
325
end: Y.RelativePosition;
326
userId: string;
327
}
328
329
class SelectionTracker {
330
private doc: Y.Doc;
331
private selections: Map<string, SelectionRange>;
332
333
constructor(doc: Y.Doc) {
334
this.doc = doc;
335
this.selections = new Map();
336
}
337
338
setSelection(userId: string, type: Y.AbstractType<any>, startIndex: number, endIndex: number) {
339
const start = Y.createRelativePositionFromTypeIndex(type, startIndex);
340
const end = Y.createRelativePositionFromTypeIndex(type, endIndex);
341
342
this.selections.set(userId, { start, end, userId });
343
}
344
345
getSelection(userId: string): { start: number; end: number } | null {
346
const selection = this.selections.get(userId);
347
if (!selection) return null;
348
349
const startAbs = Y.createAbsolutePositionFromRelativePosition(selection.start, this.doc);
350
const endAbs = Y.createAbsolutePositionFromRelativePosition(selection.end, this.doc);
351
352
if (!startAbs || !endAbs) return null;
353
354
return {
355
start: startAbs.index,
356
end: endAbs.index
357
};
358
}
359
}
360
```
361
362
**Position Persistence:**
363
364
```typescript
365
import * as Y from "yjs";
366
367
// Save positions to storage
368
function savePositions(positions: Map<string, Y.RelativePosition>): string {
369
const serialized = Array.from(positions.entries()).map(([key, pos]) => ({
370
key,
371
position: Y.relativePositionToJSON(pos)
372
}));
373
374
return JSON.stringify(serialized);
375
}
376
377
// Restore positions from storage
378
function loadPositions(data: string): Map<string, Y.RelativePosition> {
379
const serialized = JSON.parse(data);
380
const positions = new Map<string, Y.RelativePosition>();
381
382
serialized.forEach(({ key, position }) => {
383
const relPos = Y.createRelativePositionFromJSON(position);
384
positions.set(key, relPos);
385
});
386
387
return positions;
388
}
389
390
// Usage
391
const doc = new Y.Doc();
392
const ytext = doc.getText("document");
393
ytext.insert(0, "Sample text");
394
395
const positions = new Map();
396
positions.set("cursor1", Y.createRelativePositionFromTypeIndex(ytext, 7));
397
positions.set("cursor2", Y.createRelativePositionFromTypeIndex(ytext, 12));
398
399
// Save to storage
400
const saved = savePositions(positions);
401
localStorage.setItem("positions", saved);
402
403
// Later: restore from storage
404
const restored = loadPositions(localStorage.getItem("positions")!);
405
406
// Positions remain valid across sessions
407
restored.forEach((relPos, key) => {
408
const absPos = Y.createAbsolutePositionFromRelativePosition(relPos, doc);
409
console.log(`${key} at index:`, absPos?.index);
410
});
411
```