mirror of
				https://github.com/BookStackApp/BookStack.git
				synced 2025-11-04 13:31:45 +03:00 
			
		
		
		
	Imported at 0.17.1, Modified to work in-app. Added & configured test dependancies. Tests need to be altered to avoid using non-included deps including react dependancies.
		
			
				
	
	
		
			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;
 | 
						|
}
 |