2 * Copyright (c) Meta Platforms, Inc. and affiliates.
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
9 import type {Binding} from '.';
10 import type {CollabElementNode} from './CollabElementNode';
11 import type {NodeKey, NodeMap, TextNode} from 'lexical';
12 import type {Map as YMap} from 'yjs';
20 import invariant from 'lexical/shared/invariant';
21 import simpleDiffWithCursor from 'lexical/shared/simpleDiffWithCursor';
23 import {syncPropertiesFromLexical, syncPropertiesFromYjs} from './Utils';
25 function $diffTextContentAndApplyDelta(
26 collabNode: CollabTextNode,
31 const selection = $getSelection();
32 let cursorOffset = nextText.length;
34 if ($isRangeSelection(selection) && selection.isCollapsed()) {
35 const anchor = selection.anchor;
37 if (anchor.key === key) {
38 cursorOffset = anchor.offset;
42 const diff = simpleDiffWithCursor(prevText, nextText, cursorOffset);
43 collabNode.spliceText(diff.index, diff.remove, diff.insert);
46 export class CollabTextNode {
49 _parent: CollabElementNode;
57 parent: CollabElementNode,
62 this._parent = parent;
65 this._normalized = false;
68 getPrevNode(nodeMap: null | NodeMap): null | TextNode {
69 if (nodeMap === null) {
73 const node = nodeMap.get(this._key);
74 return $isTextNode(node) ? node : null;
77 getNode(): null | TextNode {
78 const node = $getNodeByKey(this._key);
79 return $isTextNode(node) ? node : null;
82 getSharedType(): YMap<unknown> {
95 return this._text.length + (this._normalized ? 0 : 1);
99 const collabElementNode = this._parent;
100 return collabElementNode.getChildOffset(this);
103 spliceText(index: number, delCount: number, newText: string): void {
104 const collabElementNode = this._parent;
105 const xmlText = collabElementNode._xmlText;
106 const offset = this.getOffset() + 1 + index;
108 if (delCount !== 0) {
109 xmlText.delete(offset, delCount);
112 if (newText !== '') {
113 xmlText.insert(offset, newText);
117 syncPropertiesAndTextFromLexical(
119 nextLexicalNode: TextNode,
120 prevNodeMap: null | NodeMap,
122 const prevLexicalNode = this.getPrevNode(prevNodeMap);
123 const nextText = nextLexicalNode.__text;
125 syncPropertiesFromLexical(
132 if (prevLexicalNode !== null) {
133 const prevText = prevLexicalNode.__text;
135 if (prevText !== nextText) {
136 const key = nextLexicalNode.__key;
137 $diffTextContentAndApplyDelta(this, key, prevText, nextText);
138 this._text = nextText;
143 syncPropertiesAndTextFromYjs(
145 keysChanged: null | Set<string>,
147 const lexicalNode = this.getNode();
149 lexicalNode !== null,
150 'syncPropertiesAndTextFromYjs: could not find decorator node',
153 syncPropertiesFromYjs(binding, this._map, lexicalNode, keysChanged);
155 const collabText = this._text;
157 if (lexicalNode.__text !== collabText) {
158 const writable = lexicalNode.getWritable();
159 writable.__text = collabText;
163 destroy(binding: Binding): void {
164 const collabNodeMap = binding.collabNodeMap;
165 collabNodeMap.delete(this._key);
169 export function $createCollabTextNode(
172 parent: CollabElementNode,
175 const collabNode = new CollabTextNode(map, text, parent, type);
176 map._collabNode = collabNode;