667 lines
		
	
	
		
			19 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
		
		
			
		
	
	
			667 lines
		
	
	
		
			19 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
|  | /** | ||
|  |  * Copyright (c) Meta Platforms, Inc. and affiliates. | ||
|  |  * | ||
|  |  * This source code is licensed under the MIT license found in the | ||
|  |  * LICENSE file in the root directory of this source tree. | ||
|  |  * | ||
|  |  */ | ||
|  | 
 | ||
|  | import type {Binding} from '.'; | ||
|  | import type {ElementNode, NodeKey, NodeMap} from 'lexical'; | ||
|  | import type {AbstractType, Map as YMap, XmlElement, XmlText} from 'yjs'; | ||
|  | 
 | ||
|  | import {$createChildrenArray} from '@lexical/offset'; | ||
|  | import { | ||
|  |   $getNodeByKey, | ||
|  |   $isDecoratorNode, | ||
|  |   $isElementNode, | ||
|  |   $isTextNode, | ||
|  | } from 'lexical'; | ||
|  | import invariant from 'lexical/shared/invariant'; | ||
|  | 
 | ||
|  | import {CollabDecoratorNode} from './CollabDecoratorNode'; | ||
|  | import {CollabLineBreakNode} from './CollabLineBreakNode'; | ||
|  | import {CollabTextNode} from './CollabTextNode'; | ||
|  | import { | ||
|  |   $createCollabNodeFromLexicalNode, | ||
|  |   $getNodeByKeyOrThrow, | ||
|  |   $getOrInitCollabNodeFromSharedType, | ||
|  |   createLexicalNodeFromCollabNode, | ||
|  |   getPositionFromElementAndOffset, | ||
|  |   removeFromParent, | ||
|  |   spliceString, | ||
|  |   syncPropertiesFromLexical, | ||
|  |   syncPropertiesFromYjs, | ||
|  | } from './Utils'; | ||
|  | 
 | ||
|  | type IntentionallyMarkedAsDirtyElement = boolean; | ||
|  | 
 | ||
|  | export class CollabElementNode { | ||
|  |   _key: NodeKey; | ||
|  |   _children: Array< | ||
|  |     | CollabElementNode | ||
|  |     | CollabTextNode | ||
|  |     | CollabDecoratorNode | ||
|  |     | CollabLineBreakNode | ||
|  |   >; | ||
|  |   _xmlText: XmlText; | ||
|  |   _type: string; | ||
|  |   _parent: null | CollabElementNode; | ||
|  | 
 | ||
|  |   constructor( | ||
|  |     xmlText: XmlText, | ||
|  |     parent: null | CollabElementNode, | ||
|  |     type: string, | ||
|  |   ) { | ||
|  |     this._key = ''; | ||
|  |     this._children = []; | ||
|  |     this._xmlText = xmlText; | ||
|  |     this._type = type; | ||
|  |     this._parent = parent; | ||
|  |   } | ||
|  | 
 | ||
|  |   getPrevNode(nodeMap: null | NodeMap): null | ElementNode { | ||
|  |     if (nodeMap === null) { | ||
|  |       return null; | ||
|  |     } | ||
|  | 
 | ||
|  |     const node = nodeMap.get(this._key); | ||
|  |     return $isElementNode(node) ? node : null; | ||
|  |   } | ||
|  | 
 | ||
|  |   getNode(): null | ElementNode { | ||
|  |     const node = $getNodeByKey(this._key); | ||
|  |     return $isElementNode(node) ? node : null; | ||
|  |   } | ||
|  | 
 | ||
|  |   getSharedType(): XmlText { | ||
|  |     return this._xmlText; | ||
|  |   } | ||
|  | 
 | ||
|  |   getType(): string { | ||
|  |     return this._type; | ||
|  |   } | ||
|  | 
 | ||
|  |   getKey(): NodeKey { | ||
|  |     return this._key; | ||
|  |   } | ||
|  | 
 | ||
|  |   isEmpty(): boolean { | ||
|  |     return this._children.length === 0; | ||
|  |   } | ||
|  | 
 | ||
|  |   getSize(): number { | ||
|  |     return 1; | ||
|  |   } | ||
|  | 
 | ||
|  |   getOffset(): number { | ||
|  |     const collabElementNode = this._parent; | ||
|  |     invariant( | ||
|  |       collabElementNode !== null, | ||
|  |       'getOffset: could not find collab element node', | ||
|  |     ); | ||
|  | 
 | ||
|  |     return collabElementNode.getChildOffset(this); | ||
|  |   } | ||
|  | 
 | ||
|  |   syncPropertiesFromYjs( | ||
|  |     binding: Binding, | ||
|  |     keysChanged: null | Set<string>, | ||
|  |   ): void { | ||
|  |     const lexicalNode = this.getNode(); | ||
|  |     invariant( | ||
|  |       lexicalNode !== null, | ||
|  |       'syncPropertiesFromYjs: could not find element node', | ||
|  |     ); | ||
|  |     syncPropertiesFromYjs(binding, this._xmlText, lexicalNode, keysChanged); | ||
|  |   } | ||
|  | 
 | ||
|  |   applyChildrenYjsDelta( | ||
|  |     binding: Binding, | ||
|  |     deltas: Array<{ | ||
|  |       insert?: string | object | AbstractType<unknown>; | ||
|  |       delete?: number; | ||
|  |       retain?: number; | ||
|  |       attributes?: { | ||
|  |         [x: string]: unknown; | ||
|  |       }; | ||
|  |     }>, | ||
|  |   ): void { | ||
|  |     const children = this._children; | ||
|  |     let currIndex = 0; | ||
|  | 
 | ||
|  |     for (let i = 0; i < deltas.length; i++) { | ||
|  |       const delta = deltas[i]; | ||
|  |       const insertDelta = delta.insert; | ||
|  |       const deleteDelta = delta.delete; | ||
|  | 
 | ||
|  |       if (delta.retain != null) { | ||
|  |         currIndex += delta.retain; | ||
|  |       } else if (typeof deleteDelta === 'number') { | ||
|  |         let deletionSize = deleteDelta; | ||
|  | 
 | ||
|  |         while (deletionSize > 0) { | ||
|  |           const {node, nodeIndex, offset, length} = | ||
|  |             getPositionFromElementAndOffset(this, currIndex, false); | ||
|  | 
 | ||
|  |           if ( | ||
|  |             node instanceof CollabElementNode || | ||
|  |             node instanceof CollabLineBreakNode || | ||
|  |             node instanceof CollabDecoratorNode | ||
|  |           ) { | ||
|  |             children.splice(nodeIndex, 1); | ||
|  |             deletionSize -= 1; | ||
|  |           } else if (node instanceof CollabTextNode) { | ||
|  |             const delCount = Math.min(deletionSize, length); | ||
|  |             const prevCollabNode = | ||
|  |               nodeIndex !== 0 ? children[nodeIndex - 1] : null; | ||
|  |             const nodeSize = node.getSize(); | ||
|  | 
 | ||
|  |             if ( | ||
|  |               offset === 0 && | ||
|  |               delCount === 1 && | ||
|  |               nodeIndex > 0 && | ||
|  |               prevCollabNode instanceof CollabTextNode && | ||
|  |               length === nodeSize && | ||
|  |               // If the node has no keys, it's been deleted
 | ||
|  |               Array.from(node._map.keys()).length === 0 | ||
|  |             ) { | ||
|  |               // Merge the text node with previous.
 | ||
|  |               prevCollabNode._text += node._text; | ||
|  |               children.splice(nodeIndex, 1); | ||
|  |             } else if (offset === 0 && delCount === nodeSize) { | ||
|  |               // The entire thing needs removing
 | ||
|  |               children.splice(nodeIndex, 1); | ||
|  |             } else { | ||
|  |               node._text = spliceString(node._text, offset, delCount, ''); | ||
|  |             } | ||
|  | 
 | ||
|  |             deletionSize -= delCount; | ||
|  |           } else { | ||
|  |             // Can occur due to the deletion from the dangling text heuristic below.
 | ||
|  |             break; | ||
|  |           } | ||
|  |         } | ||
|  |       } else if (insertDelta != null) { | ||
|  |         if (typeof insertDelta === 'string') { | ||
|  |           const {node, offset} = getPositionFromElementAndOffset( | ||
|  |             this, | ||
|  |             currIndex, | ||
|  |             true, | ||
|  |           ); | ||
|  | 
 | ||
|  |           if (node instanceof CollabTextNode) { | ||
|  |             node._text = spliceString(node._text, offset, 0, insertDelta); | ||
|  |           } else { | ||
|  |             // TODO: maybe we can improve this by keeping around a redundant
 | ||
|  |             // text node map, rather than removing all the text nodes, so there
 | ||
|  |             // never can be dangling text.
 | ||
|  | 
 | ||
|  |             // We have a conflict where there was likely a CollabTextNode and
 | ||
|  |             // an Lexical TextNode too, but they were removed in a merge. So
 | ||
|  |             // let's just ignore the text and trigger a removal for it from our
 | ||
|  |             // shared type.
 | ||
|  |             this._xmlText.delete(offset, insertDelta.length); | ||
|  |           } | ||
|  | 
 | ||
|  |           currIndex += insertDelta.length; | ||
|  |         } else { | ||
|  |           const sharedType = insertDelta; | ||
|  |           const {nodeIndex} = getPositionFromElementAndOffset( | ||
|  |             this, | ||
|  |             currIndex, | ||
|  |             false, | ||
|  |           ); | ||
|  |           const collabNode = $getOrInitCollabNodeFromSharedType( | ||
|  |             binding, | ||
|  |             sharedType as XmlText | YMap<unknown> | XmlElement, | ||
|  |             this, | ||
|  |           ); | ||
|  |           children.splice(nodeIndex, 0, collabNode); | ||
|  |           currIndex += 1; | ||
|  |         } | ||
|  |       } else { | ||
|  |         throw new Error('Unexpected delta format'); | ||
|  |       } | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   syncChildrenFromYjs(binding: Binding): void { | ||
|  |     // Now diff the children of the collab node with that of our existing Lexical node.
 | ||
|  |     const lexicalNode = this.getNode(); | ||
|  |     invariant( | ||
|  |       lexicalNode !== null, | ||
|  |       'syncChildrenFromYjs: could not find element node', | ||
|  |     ); | ||
|  | 
 | ||
|  |     const key = lexicalNode.__key; | ||
|  |     const prevLexicalChildrenKeys = $createChildrenArray(lexicalNode, null); | ||
|  |     const nextLexicalChildrenKeys: Array<NodeKey> = []; | ||
|  |     const lexicalChildrenKeysLength = prevLexicalChildrenKeys.length; | ||
|  |     const collabChildren = this._children; | ||
|  |     const collabChildrenLength = collabChildren.length; | ||
|  |     const collabNodeMap = binding.collabNodeMap; | ||
|  |     const visitedKeys = new Set(); | ||
|  |     let collabKeys; | ||
|  |     let writableLexicalNode; | ||
|  |     let prevIndex = 0; | ||
|  |     let prevChildNode = null; | ||
|  | 
 | ||
|  |     if (collabChildrenLength !== lexicalChildrenKeysLength) { | ||
|  |       writableLexicalNode = lexicalNode.getWritable(); | ||
|  |     } | ||
|  | 
 | ||
|  |     for (let i = 0; i < collabChildrenLength; i++) { | ||
|  |       const lexicalChildKey = prevLexicalChildrenKeys[prevIndex]; | ||
|  |       const childCollabNode = collabChildren[i]; | ||
|  |       const collabLexicalChildNode = childCollabNode.getNode(); | ||
|  |       const collabKey = childCollabNode._key; | ||
|  | 
 | ||
|  |       if (collabLexicalChildNode !== null && lexicalChildKey === collabKey) { | ||
|  |         const childNeedsUpdating = $isTextNode(collabLexicalChildNode); | ||
|  |         // Update
 | ||
|  |         visitedKeys.add(lexicalChildKey); | ||
|  | 
 | ||
|  |         if (childNeedsUpdating) { | ||
|  |           childCollabNode._key = lexicalChildKey; | ||
|  | 
 | ||
|  |           if (childCollabNode instanceof CollabElementNode) { | ||
|  |             const xmlText = childCollabNode._xmlText; | ||
|  |             childCollabNode.syncPropertiesFromYjs(binding, null); | ||
|  |             childCollabNode.applyChildrenYjsDelta(binding, xmlText.toDelta()); | ||
|  |             childCollabNode.syncChildrenFromYjs(binding); | ||
|  |           } else if (childCollabNode instanceof CollabTextNode) { | ||
|  |             childCollabNode.syncPropertiesAndTextFromYjs(binding, null); | ||
|  |           } else if (childCollabNode instanceof CollabDecoratorNode) { | ||
|  |             childCollabNode.syncPropertiesFromYjs(binding, null); | ||
|  |           } else if (!(childCollabNode instanceof CollabLineBreakNode)) { | ||
|  |             invariant( | ||
|  |               false, | ||
|  |               'syncChildrenFromYjs: expected text, element, decorator, or linebreak collab node', | ||
|  |             ); | ||
|  |           } | ||
|  |         } | ||
|  | 
 | ||
|  |         nextLexicalChildrenKeys[i] = lexicalChildKey; | ||
|  |         prevChildNode = collabLexicalChildNode; | ||
|  |         prevIndex++; | ||
|  |       } else { | ||
|  |         if (collabKeys === undefined) { | ||
|  |           collabKeys = new Set(); | ||
|  | 
 | ||
|  |           for (let s = 0; s < collabChildrenLength; s++) { | ||
|  |             const child = collabChildren[s]; | ||
|  |             const childKey = child._key; | ||
|  | 
 | ||
|  |             if (childKey !== '') { | ||
|  |               collabKeys.add(childKey); | ||
|  |             } | ||
|  |           } | ||
|  |         } | ||
|  | 
 | ||
|  |         if ( | ||
|  |           collabLexicalChildNode !== null && | ||
|  |           lexicalChildKey !== undefined && | ||
|  |           !collabKeys.has(lexicalChildKey) | ||
|  |         ) { | ||
|  |           const nodeToRemove = $getNodeByKeyOrThrow(lexicalChildKey); | ||
|  |           removeFromParent(nodeToRemove); | ||
|  |           i--; | ||
|  |           prevIndex++; | ||
|  |           continue; | ||
|  |         } | ||
|  | 
 | ||
|  |         writableLexicalNode = lexicalNode.getWritable(); | ||
|  |         // Create/Replace
 | ||
|  |         const lexicalChildNode = createLexicalNodeFromCollabNode( | ||
|  |           binding, | ||
|  |           childCollabNode, | ||
|  |           key, | ||
|  |         ); | ||
|  |         const childKey = lexicalChildNode.__key; | ||
|  |         collabNodeMap.set(childKey, childCollabNode); | ||
|  |         nextLexicalChildrenKeys[i] = childKey; | ||
|  |         if (prevChildNode === null) { | ||
|  |           const nextSibling = writableLexicalNode.getFirstChild(); | ||
|  |           writableLexicalNode.__first = childKey; | ||
|  |           if (nextSibling !== null) { | ||
|  |             const writableNextSibling = nextSibling.getWritable(); | ||
|  |             writableNextSibling.__prev = childKey; | ||
|  |             lexicalChildNode.__next = writableNextSibling.__key; | ||
|  |           } | ||
|  |         } else { | ||
|  |           const writablePrevChildNode = prevChildNode.getWritable(); | ||
|  |           const nextSibling = prevChildNode.getNextSibling(); | ||
|  |           writablePrevChildNode.__next = childKey; | ||
|  |           lexicalChildNode.__prev = prevChildNode.__key; | ||
|  |           if (nextSibling !== null) { | ||
|  |             const writableNextSibling = nextSibling.getWritable(); | ||
|  |             writableNextSibling.__prev = childKey; | ||
|  |             lexicalChildNode.__next = writableNextSibling.__key; | ||
|  |           } | ||
|  |         } | ||
|  |         if (i === collabChildrenLength - 1) { | ||
|  |           writableLexicalNode.__last = childKey; | ||
|  |         } | ||
|  |         writableLexicalNode.__size++; | ||
|  |         prevChildNode = lexicalChildNode; | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     for (let i = 0; i < lexicalChildrenKeysLength; i++) { | ||
|  |       const lexicalChildKey = prevLexicalChildrenKeys[i]; | ||
|  | 
 | ||
|  |       if (!visitedKeys.has(lexicalChildKey)) { | ||
|  |         // Remove
 | ||
|  |         const lexicalChildNode = $getNodeByKeyOrThrow(lexicalChildKey); | ||
|  |         const collabNode = binding.collabNodeMap.get(lexicalChildKey); | ||
|  | 
 | ||
|  |         if (collabNode !== undefined) { | ||
|  |           collabNode.destroy(binding); | ||
|  |         } | ||
|  |         removeFromParent(lexicalChildNode); | ||
|  |       } | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   syncPropertiesFromLexical( | ||
|  |     binding: Binding, | ||
|  |     nextLexicalNode: ElementNode, | ||
|  |     prevNodeMap: null | NodeMap, | ||
|  |   ): void { | ||
|  |     syncPropertiesFromLexical( | ||
|  |       binding, | ||
|  |       this._xmlText, | ||
|  |       this.getPrevNode(prevNodeMap), | ||
|  |       nextLexicalNode, | ||
|  |     ); | ||
|  |   } | ||
|  | 
 | ||
|  |   _syncChildFromLexical( | ||
|  |     binding: Binding, | ||
|  |     index: number, | ||
|  |     key: NodeKey, | ||
|  |     prevNodeMap: null | NodeMap, | ||
|  |     dirtyElements: null | Map<NodeKey, IntentionallyMarkedAsDirtyElement>, | ||
|  |     dirtyLeaves: null | Set<NodeKey>, | ||
|  |   ): void { | ||
|  |     const childCollabNode = this._children[index]; | ||
|  |     // Update
 | ||
|  |     const nextChildNode = $getNodeByKeyOrThrow(key); | ||
|  | 
 | ||
|  |     if ( | ||
|  |       childCollabNode instanceof CollabElementNode && | ||
|  |       $isElementNode(nextChildNode) | ||
|  |     ) { | ||
|  |       childCollabNode.syncPropertiesFromLexical( | ||
|  |         binding, | ||
|  |         nextChildNode, | ||
|  |         prevNodeMap, | ||
|  |       ); | ||
|  |       childCollabNode.syncChildrenFromLexical( | ||
|  |         binding, | ||
|  |         nextChildNode, | ||
|  |         prevNodeMap, | ||
|  |         dirtyElements, | ||
|  |         dirtyLeaves, | ||
|  |       ); | ||
|  |     } else if ( | ||
|  |       childCollabNode instanceof CollabTextNode && | ||
|  |       $isTextNode(nextChildNode) | ||
|  |     ) { | ||
|  |       childCollabNode.syncPropertiesAndTextFromLexical( | ||
|  |         binding, | ||
|  |         nextChildNode, | ||
|  |         prevNodeMap, | ||
|  |       ); | ||
|  |     } else if ( | ||
|  |       childCollabNode instanceof CollabDecoratorNode && | ||
|  |       $isDecoratorNode(nextChildNode) | ||
|  |     ) { | ||
|  |       childCollabNode.syncPropertiesFromLexical( | ||
|  |         binding, | ||
|  |         nextChildNode, | ||
|  |         prevNodeMap, | ||
|  |       ); | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   syncChildrenFromLexical( | ||
|  |     binding: Binding, | ||
|  |     nextLexicalNode: ElementNode, | ||
|  |     prevNodeMap: null | NodeMap, | ||
|  |     dirtyElements: null | Map<NodeKey, IntentionallyMarkedAsDirtyElement>, | ||
|  |     dirtyLeaves: null | Set<NodeKey>, | ||
|  |   ): void { | ||
|  |     const prevLexicalNode = this.getPrevNode(prevNodeMap); | ||
|  |     const prevChildren = | ||
|  |       prevLexicalNode === null | ||
|  |         ? [] | ||
|  |         : $createChildrenArray(prevLexicalNode, prevNodeMap); | ||
|  |     const nextChildren = $createChildrenArray(nextLexicalNode, null); | ||
|  |     const prevEndIndex = prevChildren.length - 1; | ||
|  |     const nextEndIndex = nextChildren.length - 1; | ||
|  |     const collabNodeMap = binding.collabNodeMap; | ||
|  |     let prevChildrenSet: Set<NodeKey> | undefined; | ||
|  |     let nextChildrenSet: Set<NodeKey> | undefined; | ||
|  |     let prevIndex = 0; | ||
|  |     let nextIndex = 0; | ||
|  | 
 | ||
|  |     while (prevIndex <= prevEndIndex && nextIndex <= nextEndIndex) { | ||
|  |       const prevKey = prevChildren[prevIndex]; | ||
|  |       const nextKey = nextChildren[nextIndex]; | ||
|  | 
 | ||
|  |       if (prevKey === nextKey) { | ||
|  |         // Nove move, create or remove
 | ||
|  |         this._syncChildFromLexical( | ||
|  |           binding, | ||
|  |           nextIndex, | ||
|  |           nextKey, | ||
|  |           prevNodeMap, | ||
|  |           dirtyElements, | ||
|  |           dirtyLeaves, | ||
|  |         ); | ||
|  | 
 | ||
|  |         prevIndex++; | ||
|  |         nextIndex++; | ||
|  |       } else { | ||
|  |         if (prevChildrenSet === undefined) { | ||
|  |           prevChildrenSet = new Set(prevChildren); | ||
|  |         } | ||
|  | 
 | ||
|  |         if (nextChildrenSet === undefined) { | ||
|  |           nextChildrenSet = new Set(nextChildren); | ||
|  |         } | ||
|  | 
 | ||
|  |         const nextHasPrevKey = nextChildrenSet.has(prevKey); | ||
|  |         const prevHasNextKey = prevChildrenSet.has(nextKey); | ||
|  | 
 | ||
|  |         if (!nextHasPrevKey) { | ||
|  |           // Remove
 | ||
|  |           this.splice(binding, nextIndex, 1); | ||
|  |           prevIndex++; | ||
|  |         } else { | ||
|  |           // Create or replace
 | ||
|  |           const nextChildNode = $getNodeByKeyOrThrow(nextKey); | ||
|  |           const collabNode = $createCollabNodeFromLexicalNode( | ||
|  |             binding, | ||
|  |             nextChildNode, | ||
|  |             this, | ||
|  |           ); | ||
|  |           collabNodeMap.set(nextKey, collabNode); | ||
|  | 
 | ||
|  |           if (prevHasNextKey) { | ||
|  |             this.splice(binding, nextIndex, 1, collabNode); | ||
|  |             prevIndex++; | ||
|  |             nextIndex++; | ||
|  |           } else { | ||
|  |             this.splice(binding, nextIndex, 0, collabNode); | ||
|  |             nextIndex++; | ||
|  |           } | ||
|  |         } | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     const appendNewChildren = prevIndex > prevEndIndex; | ||
|  |     const removeOldChildren = nextIndex > nextEndIndex; | ||
|  | 
 | ||
|  |     if (appendNewChildren && !removeOldChildren) { | ||
|  |       for (; nextIndex <= nextEndIndex; ++nextIndex) { | ||
|  |         const key = nextChildren[nextIndex]; | ||
|  |         const nextChildNode = $getNodeByKeyOrThrow(key); | ||
|  |         const collabNode = $createCollabNodeFromLexicalNode( | ||
|  |           binding, | ||
|  |           nextChildNode, | ||
|  |           this, | ||
|  |         ); | ||
|  |         this.append(collabNode); | ||
|  |         collabNodeMap.set(key, collabNode); | ||
|  |       } | ||
|  |     } else if (removeOldChildren && !appendNewChildren) { | ||
|  |       for (let i = this._children.length - 1; i >= nextIndex; i--) { | ||
|  |         this.splice(binding, i, 1); | ||
|  |       } | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   append( | ||
|  |     collabNode: | ||
|  |       | CollabElementNode | ||
|  |       | CollabDecoratorNode | ||
|  |       | CollabTextNode | ||
|  |       | CollabLineBreakNode, | ||
|  |   ): void { | ||
|  |     const xmlText = this._xmlText; | ||
|  |     const children = this._children; | ||
|  |     const lastChild = children[children.length - 1]; | ||
|  |     const offset = | ||
|  |       lastChild !== undefined ? lastChild.getOffset() + lastChild.getSize() : 0; | ||
|  | 
 | ||
|  |     if (collabNode instanceof CollabElementNode) { | ||
|  |       xmlText.insertEmbed(offset, collabNode._xmlText); | ||
|  |     } else if (collabNode instanceof CollabTextNode) { | ||
|  |       const map = collabNode._map; | ||
|  | 
 | ||
|  |       if (map.parent === null) { | ||
|  |         xmlText.insertEmbed(offset, map); | ||
|  |       } | ||
|  | 
 | ||
|  |       xmlText.insert(offset + 1, collabNode._text); | ||
|  |     } else if (collabNode instanceof CollabLineBreakNode) { | ||
|  |       xmlText.insertEmbed(offset, collabNode._map); | ||
|  |     } else if (collabNode instanceof CollabDecoratorNode) { | ||
|  |       xmlText.insertEmbed(offset, collabNode._xmlElem); | ||
|  |     } | ||
|  | 
 | ||
|  |     this._children.push(collabNode); | ||
|  |   } | ||
|  | 
 | ||
|  |   splice( | ||
|  |     binding: Binding, | ||
|  |     index: number, | ||
|  |     delCount: number, | ||
|  |     collabNode?: | ||
|  |       | CollabElementNode | ||
|  |       | CollabDecoratorNode | ||
|  |       | CollabTextNode | ||
|  |       | CollabLineBreakNode, | ||
|  |   ): void { | ||
|  |     const children = this._children; | ||
|  |     const child = children[index]; | ||
|  | 
 | ||
|  |     if (child === undefined) { | ||
|  |       invariant( | ||
|  |         collabNode !== undefined, | ||
|  |         'splice: could not find collab element node', | ||
|  |       ); | ||
|  |       this.append(collabNode); | ||
|  |       return; | ||
|  |     } | ||
|  | 
 | ||
|  |     const offset = child.getOffset(); | ||
|  |     invariant(offset !== -1, 'splice: expected offset to be greater than zero'); | ||
|  | 
 | ||
|  |     const xmlText = this._xmlText; | ||
|  | 
 | ||
|  |     if (delCount !== 0) { | ||
|  |       // What if we delete many nodes, don't we need to get all their
 | ||
|  |       // sizes?
 | ||
|  |       xmlText.delete(offset, child.getSize()); | ||
|  |     } | ||
|  | 
 | ||
|  |     if (collabNode instanceof CollabElementNode) { | ||
|  |       xmlText.insertEmbed(offset, collabNode._xmlText); | ||
|  |     } else if (collabNode instanceof CollabTextNode) { | ||
|  |       const map = collabNode._map; | ||
|  | 
 | ||
|  |       if (map.parent === null) { | ||
|  |         xmlText.insertEmbed(offset, map); | ||
|  |       } | ||
|  | 
 | ||
|  |       xmlText.insert(offset + 1, collabNode._text); | ||
|  |     } else if (collabNode instanceof CollabLineBreakNode) { | ||
|  |       xmlText.insertEmbed(offset, collabNode._map); | ||
|  |     } else if (collabNode instanceof CollabDecoratorNode) { | ||
|  |       xmlText.insertEmbed(offset, collabNode._xmlElem); | ||
|  |     } | ||
|  | 
 | ||
|  |     if (delCount !== 0) { | ||
|  |       const childrenToDelete = children.slice(index, index + delCount); | ||
|  | 
 | ||
|  |       for (let i = 0; i < childrenToDelete.length; i++) { | ||
|  |         childrenToDelete[i].destroy(binding); | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     if (collabNode !== undefined) { | ||
|  |       children.splice(index, delCount, collabNode); | ||
|  |     } else { | ||
|  |       children.splice(index, delCount); | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   getChildOffset( | ||
|  |     collabNode: | ||
|  |       | CollabElementNode | ||
|  |       | CollabTextNode | ||
|  |       | CollabDecoratorNode | ||
|  |       | CollabLineBreakNode, | ||
|  |   ): number { | ||
|  |     let offset = 0; | ||
|  |     const children = this._children; | ||
|  | 
 | ||
|  |     for (let i = 0; i < children.length; i++) { | ||
|  |       const child = children[i]; | ||
|  | 
 | ||
|  |       if (child === collabNode) { | ||
|  |         return offset; | ||
|  |       } | ||
|  | 
 | ||
|  |       offset += child.getSize(); | ||
|  |     } | ||
|  | 
 | ||
|  |     return -1; | ||
|  |   } | ||
|  | 
 | ||
|  |   destroy(binding: Binding): void { | ||
|  |     const collabNodeMap = binding.collabNodeMap; | ||
|  |     const children = this._children; | ||
|  | 
 | ||
|  |     for (let i = 0; i < children.length; i++) { | ||
|  |       children[i].destroy(binding); | ||
|  |     } | ||
|  | 
 | ||
|  |     collabNodeMap.delete(this._key); | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | export function $createCollabElementNode( | ||
|  |   xmlText: XmlText, | ||
|  |   parent: null | CollabElementNode, | ||
|  |   type: string, | ||
|  | ): CollabElementNode { | ||
|  |   const collabNode = new CollabElementNode(xmlText, parent, type); | ||
|  |   xmlText._collabNode = collabNode; | ||
|  |   return collabNode; | ||
|  | } |