@lexical/offset 0.44.1-nightly.20260519.0 → 0.45.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/package.json CHANGED
@@ -8,34 +8,49 @@
8
8
  "offset"
9
9
  ],
10
10
  "license": "MIT",
11
- "version": "0.44.1-nightly.20260519.0",
12
- "main": "LexicalOffset.js",
13
- "types": "index.d.ts",
11
+ "version": "0.45.0",
12
+ "main": "./dist/LexicalOffset.js",
13
+ "types": "./dist/index.d.ts",
14
14
  "repository": {
15
15
  "type": "git",
16
16
  "url": "git+https://github.com/facebook/lexical.git",
17
17
  "directory": "packages/lexical-offset"
18
18
  },
19
- "module": "LexicalOffset.mjs",
19
+ "module": "./dist/LexicalOffset.mjs",
20
20
  "sideEffects": false,
21
21
  "exports": {
22
22
  ".": {
23
+ "source": "./src/index.ts",
23
24
  "import": {
24
- "types": "./index.d.ts",
25
- "development": "./LexicalOffset.dev.mjs",
26
- "production": "./LexicalOffset.prod.mjs",
27
- "node": "./LexicalOffset.node.mjs",
28
- "default": "./LexicalOffset.mjs"
25
+ "types": "./dist/index.d.ts",
26
+ "development": "./dist/LexicalOffset.dev.mjs",
27
+ "production": "./dist/LexicalOffset.prod.mjs",
28
+ "node": "./dist/LexicalOffset.node.mjs",
29
+ "default": "./dist/LexicalOffset.mjs"
29
30
  },
30
31
  "require": {
31
- "types": "./index.d.ts",
32
- "development": "./LexicalOffset.dev.js",
33
- "production": "./LexicalOffset.prod.js",
34
- "default": "./LexicalOffset.js"
32
+ "types": "./dist/index.d.ts",
33
+ "development": "./dist/LexicalOffset.dev.js",
34
+ "production": "./dist/LexicalOffset.prod.js",
35
+ "default": "./dist/LexicalOffset.js"
35
36
  }
36
37
  }
37
38
  },
38
39
  "dependencies": {
39
- "lexical": "0.44.1-nightly.20260519.0"
40
- }
40
+ "lexical": "0.45.0",
41
+ "@lexical/internal": "0.45.0"
42
+ },
43
+ "files": [
44
+ "dist",
45
+ "src",
46
+ "!src/__tests__",
47
+ "!src/__bench__",
48
+ "!src/__mocks__",
49
+ "!src/**/*.test.ts",
50
+ "!src/**/*.test.tsx",
51
+ "!src/**/*.bench.ts",
52
+ "!src/**/*.bench.tsx",
53
+ "README.md",
54
+ "LICENSE"
55
+ ]
41
56
  }
package/src/index.ts ADDED
@@ -0,0 +1,572 @@
1
+ /**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+
9
+ import type {
10
+ EditorState,
11
+ LexicalEditor,
12
+ NodeKey,
13
+ NodeMap,
14
+ RangeSelection,
15
+ RootNode,
16
+ } from 'lexical';
17
+
18
+ import invariant from '@lexical/internal/invariant';
19
+ import {
20
+ $createChildrenArray as $createChildrenArray_,
21
+ $createRangeSelection,
22
+ $getNodeByKey,
23
+ $isElementNode,
24
+ $isTextNode,
25
+ } from 'lexical';
26
+
27
+ type OffsetElementNode = {
28
+ child: null | OffsetNode;
29
+ end: number;
30
+ key: NodeKey;
31
+ next: null | OffsetNode;
32
+ parent: null | OffsetElementNode;
33
+ prev: null | OffsetNode;
34
+ start: number;
35
+ type: 'element';
36
+ };
37
+ type OffsetTextNode = {
38
+ child: null;
39
+ end: number;
40
+ key: NodeKey;
41
+ next: null | OffsetNode;
42
+ parent: null | OffsetElementNode;
43
+ prev: null | OffsetNode;
44
+ start: number;
45
+ type: 'text';
46
+ };
47
+ type OffsetInlineNode = {
48
+ child: null;
49
+ end: number;
50
+ key: NodeKey;
51
+ next: null | OffsetNode;
52
+ parent: null | OffsetElementNode;
53
+ prev: null | OffsetNode;
54
+ start: number;
55
+ type: 'inline';
56
+ };
57
+ type OffsetNode = OffsetElementNode | OffsetTextNode | OffsetInlineNode;
58
+ type OffsetMap = Map<NodeKey, OffsetNode>;
59
+
60
+ /** @deprecated OffsetView has never worked correctly and will be removed */
61
+ export class OffsetView {
62
+ _offsetMap: OffsetMap;
63
+ _firstNode: null | OffsetNode;
64
+ _blockOffsetSize: number;
65
+
66
+ constructor(
67
+ offsetMap: OffsetMap,
68
+ firstNode: null | OffsetNode,
69
+ blockOffsetSize = 1,
70
+ ) {
71
+ this._offsetMap = offsetMap;
72
+ this._firstNode = firstNode;
73
+ this._blockOffsetSize = blockOffsetSize;
74
+ }
75
+
76
+ createSelectionFromOffsets(
77
+ originalStart: number,
78
+ originalEnd: number,
79
+ diffOffsetView?: OffsetView,
80
+ ): null | RangeSelection {
81
+ const firstNode = this._firstNode;
82
+
83
+ if (firstNode === null) {
84
+ return null;
85
+ }
86
+
87
+ let start = originalStart;
88
+ let end = originalEnd;
89
+ let startOffsetNode = $searchForNodeWithOffset(
90
+ firstNode,
91
+ start,
92
+ this._blockOffsetSize,
93
+ );
94
+ let endOffsetNode = $searchForNodeWithOffset(
95
+ firstNode,
96
+ end,
97
+ this._blockOffsetSize,
98
+ );
99
+
100
+ if (diffOffsetView !== undefined) {
101
+ start = $getAdjustedOffsetFromDiff(
102
+ start,
103
+ startOffsetNode,
104
+ diffOffsetView,
105
+ this,
106
+ this._blockOffsetSize,
107
+ );
108
+ startOffsetNode = $searchForNodeWithOffset(
109
+ firstNode,
110
+ start,
111
+ this._blockOffsetSize,
112
+ );
113
+ end = $getAdjustedOffsetFromDiff(
114
+ end,
115
+ endOffsetNode,
116
+ diffOffsetView,
117
+ this,
118
+ this._blockOffsetSize,
119
+ );
120
+ endOffsetNode = $searchForNodeWithOffset(
121
+ firstNode,
122
+ end,
123
+ this._blockOffsetSize,
124
+ );
125
+ }
126
+
127
+ if (startOffsetNode === null || endOffsetNode === null) {
128
+ return null;
129
+ }
130
+
131
+ let startKey = startOffsetNode.key;
132
+ let endKey = endOffsetNode.key;
133
+ const startNode = $getNodeByKey(startKey);
134
+ const endNode = $getNodeByKey(endKey);
135
+
136
+ if (startNode === null || endNode === null) {
137
+ return null;
138
+ }
139
+
140
+ let startOffset = 0;
141
+ let endOffset = 0;
142
+ let startType: 'element' | 'text' = 'element';
143
+ let endType: 'element' | 'text' = 'element';
144
+
145
+ if (startOffsetNode.type === 'text') {
146
+ startOffset = start - startOffsetNode.start;
147
+ startType = 'text';
148
+ // If we are at the edge of a text node and we
149
+ // don't have a collapsed selection, then let's
150
+ // try and correct the offset node.
151
+ const sibling = startNode.getNextSibling();
152
+
153
+ if (
154
+ start !== end &&
155
+ startOffset === startNode.getTextContentSize() &&
156
+ $isTextNode(sibling)
157
+ ) {
158
+ startOffset = 0;
159
+ startKey = sibling.__key;
160
+ }
161
+ } else if (startOffsetNode.type === 'inline') {
162
+ startKey = startNode.getParentOrThrow().getKey();
163
+ startOffset =
164
+ end > startOffsetNode.start
165
+ ? startOffsetNode.end
166
+ : startOffsetNode.start;
167
+ }
168
+
169
+ if (endOffsetNode.type === 'text') {
170
+ endOffset = end - endOffsetNode.start;
171
+ endType = 'text';
172
+ } else if (endOffsetNode.type === 'inline') {
173
+ endKey = endNode.getParentOrThrow().getKey();
174
+ endOffset =
175
+ end > endOffsetNode.start ? endOffsetNode.end : endOffsetNode.start;
176
+ }
177
+
178
+ const selection = $createRangeSelection();
179
+
180
+ if (selection === null) {
181
+ return null;
182
+ }
183
+
184
+ selection.anchor.set(startKey, startOffset, startType);
185
+ selection.focus.set(endKey, endOffset, endType);
186
+
187
+ return selection;
188
+ }
189
+
190
+ getOffsetsFromSelection(selection: RangeSelection): [number, number] {
191
+ const anchor = selection.anchor;
192
+ const focus = selection.focus;
193
+ const offsetMap = this._offsetMap;
194
+ const anchorOffset = anchor.offset;
195
+ const focusOffset = focus.offset;
196
+ let start = -1;
197
+ let end = -1;
198
+
199
+ if (anchor.type === 'text') {
200
+ const offsetNode = offsetMap.get(anchor.key);
201
+
202
+ if (offsetNode !== undefined) {
203
+ start = offsetNode.start + anchorOffset;
204
+ }
205
+ } else {
206
+ const node = anchor.getNode().getDescendantByIndex(anchorOffset);
207
+
208
+ if (node !== null) {
209
+ const offsetNode = offsetMap.get(node.getKey());
210
+
211
+ if (offsetNode !== undefined) {
212
+ const isAtEnd = node.getIndexWithinParent() !== anchorOffset;
213
+ start = isAtEnd ? offsetNode.end : offsetNode.start;
214
+ }
215
+ }
216
+ }
217
+
218
+ if (focus.type === 'text') {
219
+ const offsetNode = offsetMap.get(focus.key);
220
+
221
+ if (offsetNode !== undefined) {
222
+ end = offsetNode.start + focus.offset;
223
+ }
224
+ } else {
225
+ const node = focus.getNode().getDescendantByIndex(focusOffset);
226
+
227
+ if (node !== null) {
228
+ const offsetNode = offsetMap.get(node.getKey());
229
+
230
+ if (offsetNode !== undefined) {
231
+ const isAtEnd = node.getIndexWithinParent() !== focusOffset;
232
+ end = isAtEnd ? offsetNode.end : offsetNode.start;
233
+ }
234
+ }
235
+ }
236
+
237
+ return [start, end];
238
+ }
239
+ }
240
+
241
+ function $getAdjustedOffsetFromDiff(
242
+ offset: number,
243
+ offsetNode: null | OffsetNode,
244
+ prevOffsetView: OffsetView,
245
+ offsetView: OffsetView,
246
+ blockOffsetSize: number,
247
+ ): number {
248
+ const prevOffsetMap = prevOffsetView._offsetMap;
249
+ const offsetMap = offsetView._offsetMap;
250
+ const visited = new Set();
251
+ let adjustedOffset = offset;
252
+ let currentNode = offsetNode;
253
+
254
+ while (currentNode !== null) {
255
+ const key = currentNode.key;
256
+ const prevNode = prevOffsetMap.get(key);
257
+ const diff = currentNode.end - currentNode.start;
258
+ visited.add(key);
259
+
260
+ if (prevNode === undefined) {
261
+ adjustedOffset += diff;
262
+ } else {
263
+ const prevDiff = prevNode.end - prevNode.start;
264
+
265
+ if (prevDiff !== diff) {
266
+ adjustedOffset += diff - prevDiff;
267
+ }
268
+ }
269
+
270
+ const sibling = currentNode.prev;
271
+
272
+ if (sibling !== null) {
273
+ currentNode = sibling;
274
+ continue;
275
+ }
276
+
277
+ let parent = currentNode.parent;
278
+
279
+ while (parent !== null) {
280
+ let parentSibling = parent.prev;
281
+
282
+ if (parentSibling !== null) {
283
+ const parentSiblingKey = parentSibling.key;
284
+ const prevParentSibling = prevOffsetMap.get(parentSiblingKey);
285
+ const parentDiff = parentSibling.end - parentSibling.start;
286
+ visited.add(parentSiblingKey);
287
+
288
+ if (prevParentSibling === undefined) {
289
+ adjustedOffset += parentDiff;
290
+ } else {
291
+ const prevParentDiff =
292
+ prevParentSibling.end - prevParentSibling.start;
293
+
294
+ if (prevParentDiff !== parentDiff) {
295
+ adjustedOffset += parentDiff - prevParentDiff;
296
+ }
297
+ }
298
+
299
+ parentSibling = parentSibling.prev;
300
+ }
301
+
302
+ parent = parent.parent;
303
+ }
304
+
305
+ break;
306
+ }
307
+
308
+ // Now traverse through the old offsets nodes and find any nodes we missed
309
+ // above, because they were not in the latest offset node view (they have been
310
+ // deleted).
311
+ const prevFirstNode = prevOffsetView._firstNode;
312
+
313
+ if (prevFirstNode !== null) {
314
+ currentNode = $searchForNodeWithOffset(
315
+ prevFirstNode,
316
+ offset,
317
+ blockOffsetSize,
318
+ );
319
+ let alreadyVisitedParentOfCurrentNode = false;
320
+
321
+ while (currentNode !== null) {
322
+ if (!visited.has(currentNode.key)) {
323
+ alreadyVisitedParentOfCurrentNode = true;
324
+ break;
325
+ }
326
+
327
+ currentNode = currentNode.parent;
328
+ }
329
+
330
+ if (!alreadyVisitedParentOfCurrentNode) {
331
+ while (currentNode !== null) {
332
+ const key = currentNode.key;
333
+
334
+ if (!visited.has(key)) {
335
+ const node = offsetMap.get(key);
336
+ const prevDiff = currentNode.end - currentNode.start;
337
+
338
+ if (node === undefined) {
339
+ adjustedOffset -= prevDiff;
340
+ } else {
341
+ const diff = node.end - node.start;
342
+
343
+ if (prevDiff !== diff) {
344
+ adjustedOffset += diff - prevDiff;
345
+ }
346
+ }
347
+ }
348
+
349
+ currentNode = currentNode.prev;
350
+ }
351
+ }
352
+ }
353
+
354
+ return adjustedOffset;
355
+ }
356
+
357
+ function $searchForNodeWithOffset(
358
+ firstNode: OffsetNode,
359
+ offset: number,
360
+ blockOffsetSize: number,
361
+ ): OffsetNode | null {
362
+ let currentNode = firstNode;
363
+
364
+ while (currentNode !== null) {
365
+ const end =
366
+ currentNode.end +
367
+ (currentNode.type !== 'element' || blockOffsetSize === 0 ? 1 : 0);
368
+
369
+ if (offset < end) {
370
+ const child = currentNode.child;
371
+
372
+ if (child !== null) {
373
+ currentNode = child;
374
+ continue;
375
+ }
376
+
377
+ return currentNode;
378
+ }
379
+
380
+ const sibling = currentNode.next;
381
+
382
+ if (sibling === null) {
383
+ break;
384
+ }
385
+
386
+ currentNode = sibling;
387
+ }
388
+
389
+ return null;
390
+ }
391
+
392
+ function $createInternalOffsetNode(
393
+ child: null | OffsetNode,
394
+ type: 'element' | 'text' | 'inline',
395
+ start: number,
396
+ end: number,
397
+ key: NodeKey,
398
+ parent: null | OffsetElementNode,
399
+ ): {
400
+ child: null | OffsetNode;
401
+ type: 'element' | 'text' | 'inline';
402
+ start: number;
403
+ end: number;
404
+ key: NodeKey;
405
+ parent: null | OffsetElementNode;
406
+ next: null;
407
+ prev: null;
408
+ } {
409
+ return {
410
+ child,
411
+ end,
412
+ key,
413
+ next: null,
414
+ parent,
415
+ prev: null,
416
+ start,
417
+ type,
418
+ };
419
+ }
420
+
421
+ function $createOffsetNode(
422
+ state: {
423
+ offset: number;
424
+ prevIsBlock: boolean;
425
+ },
426
+ key: NodeKey,
427
+ parent: null | OffsetElementNode,
428
+ nodeMap: NodeMap,
429
+ offsetMap: OffsetMap,
430
+ blockOffsetSize: number,
431
+ ): OffsetNode {
432
+ const node = nodeMap.get(key);
433
+
434
+ if (node === undefined) {
435
+ invariant(false, 'createOffsetModel: could not find node by key');
436
+ }
437
+
438
+ const start = state.offset;
439
+
440
+ if ($isElementNode(node)) {
441
+ const childKeys = $createChildrenArray(node, nodeMap);
442
+ const blockIsEmpty = childKeys.length === 0;
443
+ const child = blockIsEmpty
444
+ ? null
445
+ : $createOffsetChild(
446
+ state,
447
+ childKeys,
448
+ null,
449
+ nodeMap,
450
+ offsetMap,
451
+ blockOffsetSize,
452
+ );
453
+
454
+ // If the prev node was not a block or the block is empty, we should
455
+ // account for the user being able to selection the block (due to the \n).
456
+ if (!state.prevIsBlock || blockIsEmpty) {
457
+ state.prevIsBlock = true;
458
+ state.offset += blockOffsetSize;
459
+ }
460
+
461
+ const offsetNode = $createInternalOffsetNode(
462
+ child,
463
+ 'element',
464
+ start,
465
+ start,
466
+ key,
467
+ parent,
468
+ ) as OffsetElementNode;
469
+
470
+ if (child !== null) {
471
+ child.parent = offsetNode;
472
+ }
473
+
474
+ const end = state.offset;
475
+ offsetNode.end = end;
476
+ offsetMap.set(key, offsetNode);
477
+ return offsetNode;
478
+ }
479
+
480
+ state.prevIsBlock = false;
481
+
482
+ const isText = $isTextNode(node);
483
+ const length = isText ? node.__text.length : 1;
484
+ const end = (state.offset += length);
485
+
486
+ const offsetNode = $createInternalOffsetNode(
487
+ null,
488
+ isText ? 'text' : 'inline',
489
+ start,
490
+ end,
491
+ key,
492
+ parent,
493
+ );
494
+
495
+ offsetMap.set(key, offsetNode as OffsetNode);
496
+
497
+ return offsetNode as OffsetNode;
498
+ }
499
+
500
+ function $createOffsetChild(
501
+ state: {
502
+ offset: number;
503
+ prevIsBlock: boolean;
504
+ },
505
+ children: Array<NodeKey>,
506
+ parent: null | OffsetElementNode,
507
+ nodeMap: NodeMap,
508
+ offsetMap: OffsetMap,
509
+ blockOffsetSize: number,
510
+ ): OffsetNode | null {
511
+ let firstNode = null;
512
+ let currentNode = null;
513
+
514
+ const childrenLength = children.length;
515
+
516
+ for (let i = 0; i < childrenLength; i++) {
517
+ const childKey = children[i];
518
+
519
+ const offsetNode = $createOffsetNode(
520
+ state,
521
+ childKey,
522
+ parent,
523
+ nodeMap,
524
+ offsetMap,
525
+ blockOffsetSize,
526
+ );
527
+
528
+ if (currentNode === null) {
529
+ firstNode = offsetNode;
530
+ } else {
531
+ offsetNode.prev = currentNode;
532
+ currentNode.next = offsetNode;
533
+ }
534
+
535
+ currentNode = offsetNode;
536
+ }
537
+
538
+ return firstNode;
539
+ }
540
+
541
+ /** @deprecated moved to `lexical` */
542
+ export const $createChildrenArray = $createChildrenArray_;
543
+
544
+ /** @deprecated OffsetView has never worked correctly and will be removed */
545
+ export function $createOffsetView(
546
+ editor: LexicalEditor,
547
+ blockOffsetSize = 1,
548
+ editorState?: EditorState | null,
549
+ ): OffsetView {
550
+ const targetEditorState =
551
+ editorState || editor._pendingEditorState || editor._editorState;
552
+ const nodeMap = targetEditorState._nodeMap;
553
+
554
+ const root = nodeMap.get('root') as RootNode;
555
+
556
+ const offsetMap = new Map();
557
+ const state = {
558
+ offset: 0,
559
+ prevIsBlock: false,
560
+ };
561
+
562
+ const node = $createOffsetChild(
563
+ state,
564
+ $createChildrenArray(root, nodeMap),
565
+ null,
566
+ nodeMap,
567
+ offsetMap,
568
+ blockOffsetSize,
569
+ );
570
+
571
+ return new OffsetView(offsetMap, node, blockOffsetSize);
572
+ }
File without changes
File without changes
File without changes