561 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
		
		
			
		
	
	
			561 lines
		
	
	
		
			15 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, YjsNode} from '.';
							 | 
						||
| 
								 | 
							
								import type {
							 | 
						||
| 
								 | 
							
								  DecoratorNode,
							 | 
						||
| 
								 | 
							
								  EditorState,
							 | 
						||
| 
								 | 
							
								  ElementNode,
							 | 
						||
| 
								 | 
							
								  LexicalNode,
							 | 
						||
| 
								 | 
							
								  RangeSelection,
							 | 
						||
| 
								 | 
							
								  TextNode,
							 | 
						||
| 
								 | 
							
								} from 'lexical';
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								import {
							 | 
						||
| 
								 | 
							
								  $getNodeByKey,
							 | 
						||
| 
								 | 
							
								  $getRoot,
							 | 
						||
| 
								 | 
							
								  $isDecoratorNode,
							 | 
						||
| 
								 | 
							
								  $isElementNode,
							 | 
						||
| 
								 | 
							
								  $isLineBreakNode,
							 | 
						||
| 
								 | 
							
								  $isRootNode,
							 | 
						||
| 
								 | 
							
								  $isTextNode,
							 | 
						||
| 
								 | 
							
								  createEditor,
							 | 
						||
| 
								 | 
							
								  NodeKey,
							 | 
						||
| 
								 | 
							
								} from 'lexical';
							 | 
						||
| 
								 | 
							
								import invariant from 'lexical/shared/invariant';
							 | 
						||
| 
								 | 
							
								import {Doc, Map as YMap, XmlElement, XmlText} from 'yjs';
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								import {
							 | 
						||
| 
								 | 
							
								  $createCollabDecoratorNode,
							 | 
						||
| 
								 | 
							
								  CollabDecoratorNode,
							 | 
						||
| 
								 | 
							
								} from './CollabDecoratorNode';
							 | 
						||
| 
								 | 
							
								import {$createCollabElementNode, CollabElementNode} from './CollabElementNode';
							 | 
						||
| 
								 | 
							
								import {
							 | 
						||
| 
								 | 
							
								  $createCollabLineBreakNode,
							 | 
						||
| 
								 | 
							
								  CollabLineBreakNode,
							 | 
						||
| 
								 | 
							
								} from './CollabLineBreakNode';
							 | 
						||
| 
								 | 
							
								import {$createCollabTextNode, CollabTextNode} from './CollabTextNode';
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								const baseExcludedProperties = new Set<string>([
							 | 
						||
| 
								 | 
							
								  '__key',
							 | 
						||
| 
								 | 
							
								  '__parent',
							 | 
						||
| 
								 | 
							
								  '__next',
							 | 
						||
| 
								 | 
							
								  '__prev',
							 | 
						||
| 
								 | 
							
								]);
							 | 
						||
| 
								 | 
							
								const elementExcludedProperties = new Set<string>([
							 | 
						||
| 
								 | 
							
								  '__first',
							 | 
						||
| 
								 | 
							
								  '__last',
							 | 
						||
| 
								 | 
							
								  '__size',
							 | 
						||
| 
								 | 
							
								]);
							 | 
						||
| 
								 | 
							
								const rootExcludedProperties = new Set<string>(['__cachedText']);
							 | 
						||
| 
								 | 
							
								const textExcludedProperties = new Set<string>(['__text']);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								function isExcludedProperty(
							 | 
						||
| 
								 | 
							
								  name: string,
							 | 
						||
| 
								 | 
							
								  node: LexicalNode,
							 | 
						||
| 
								 | 
							
								  binding: Binding,
							 | 
						||
| 
								 | 
							
								): boolean {
							 | 
						||
| 
								 | 
							
								  if (baseExcludedProperties.has(name)) {
							 | 
						||
| 
								 | 
							
								    return true;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if ($isTextNode(node)) {
							 | 
						||
| 
								 | 
							
								    if (textExcludedProperties.has(name)) {
							 | 
						||
| 
								 | 
							
								      return true;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  } else if ($isElementNode(node)) {
							 | 
						||
| 
								 | 
							
								    if (
							 | 
						||
| 
								 | 
							
								      elementExcludedProperties.has(name) ||
							 | 
						||
| 
								 | 
							
								      ($isRootNode(node) && rootExcludedProperties.has(name))
							 | 
						||
| 
								 | 
							
								    ) {
							 | 
						||
| 
								 | 
							
								      return true;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  const nodeKlass = node.constructor;
							 | 
						||
| 
								 | 
							
								  const excludedProperties = binding.excludedProperties.get(nodeKlass);
							 | 
						||
| 
								 | 
							
								  return excludedProperties != null && excludedProperties.has(name);
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								export function getIndexOfYjsNode(
							 | 
						||
| 
								 | 
							
								  yjsParentNode: YjsNode,
							 | 
						||
| 
								 | 
							
								  yjsNode: YjsNode,
							 | 
						||
| 
								 | 
							
								): number {
							 | 
						||
| 
								 | 
							
								  let node = yjsParentNode.firstChild;
							 | 
						||
| 
								 | 
							
								  let i = -1;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (node === null) {
							 | 
						||
| 
								 | 
							
								    return -1;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  do {
							 | 
						||
| 
								 | 
							
								    i++;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (node === yjsNode) {
							 | 
						||
| 
								 | 
							
								      return i;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    // @ts-expect-error Sibling exists but type is not available from YJS.
							 | 
						||
| 
								 | 
							
								    node = node.nextSibling;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (node === null) {
							 | 
						||
| 
								 | 
							
								      return -1;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  } while (node !== null);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  return i;
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								export function $getNodeByKeyOrThrow(key: NodeKey): LexicalNode {
							 | 
						||
| 
								 | 
							
								  const node = $getNodeByKey(key);
							 | 
						||
| 
								 | 
							
								  invariant(node !== null, 'could not find node by key');
							 | 
						||
| 
								 | 
							
								  return node;
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								export function $createCollabNodeFromLexicalNode(
							 | 
						||
| 
								 | 
							
								  binding: Binding,
							 | 
						||
| 
								 | 
							
								  lexicalNode: LexicalNode,
							 | 
						||
| 
								 | 
							
								  parent: CollabElementNode,
							 | 
						||
| 
								 | 
							
								):
							 | 
						||
| 
								 | 
							
								  | CollabElementNode
							 | 
						||
| 
								 | 
							
								  | CollabTextNode
							 | 
						||
| 
								 | 
							
								  | CollabLineBreakNode
							 | 
						||
| 
								 | 
							
								  | CollabDecoratorNode {
							 | 
						||
| 
								 | 
							
								  const nodeType = lexicalNode.__type;
							 | 
						||
| 
								 | 
							
								  let collabNode;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if ($isElementNode(lexicalNode)) {
							 | 
						||
| 
								 | 
							
								    const xmlText = new XmlText();
							 | 
						||
| 
								 | 
							
								    collabNode = $createCollabElementNode(xmlText, parent, nodeType);
							 | 
						||
| 
								 | 
							
								    collabNode.syncPropertiesFromLexical(binding, lexicalNode, null);
							 | 
						||
| 
								 | 
							
								    collabNode.syncChildrenFromLexical(binding, lexicalNode, null, null, null);
							 | 
						||
| 
								 | 
							
								  } else if ($isTextNode(lexicalNode)) {
							 | 
						||
| 
								 | 
							
								    // TODO create a token text node for token, segmented nodes.
							 | 
						||
| 
								 | 
							
								    const map = new YMap();
							 | 
						||
| 
								 | 
							
								    collabNode = $createCollabTextNode(
							 | 
						||
| 
								 | 
							
								      map,
							 | 
						||
| 
								 | 
							
								      lexicalNode.__text,
							 | 
						||
| 
								 | 
							
								      parent,
							 | 
						||
| 
								 | 
							
								      nodeType,
							 | 
						||
| 
								 | 
							
								    );
							 | 
						||
| 
								 | 
							
								    collabNode.syncPropertiesAndTextFromLexical(binding, lexicalNode, null);
							 | 
						||
| 
								 | 
							
								  } else if ($isLineBreakNode(lexicalNode)) {
							 | 
						||
| 
								 | 
							
								    const map = new YMap();
							 | 
						||
| 
								 | 
							
								    map.set('__type', 'linebreak');
							 | 
						||
| 
								 | 
							
								    collabNode = $createCollabLineBreakNode(map, parent);
							 | 
						||
| 
								 | 
							
								  } else if ($isDecoratorNode(lexicalNode)) {
							 | 
						||
| 
								 | 
							
								    const xmlElem = new XmlElement();
							 | 
						||
| 
								 | 
							
								    collabNode = $createCollabDecoratorNode(xmlElem, parent, nodeType);
							 | 
						||
| 
								 | 
							
								    collabNode.syncPropertiesFromLexical(binding, lexicalNode, null);
							 | 
						||
| 
								 | 
							
								  } else {
							 | 
						||
| 
								 | 
							
								    invariant(false, 'Expected text, element, decorator, or linebreak node');
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  collabNode._key = lexicalNode.__key;
							 | 
						||
| 
								 | 
							
								  return collabNode;
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								function getNodeTypeFromSharedType(
							 | 
						||
| 
								 | 
							
								  sharedType: XmlText | YMap<unknown> | XmlElement,
							 | 
						||
| 
								 | 
							
								): string {
							 | 
						||
| 
								 | 
							
								  const type =
							 | 
						||
| 
								 | 
							
								    sharedType instanceof YMap
							 | 
						||
| 
								 | 
							
								      ? sharedType.get('__type')
							 | 
						||
| 
								 | 
							
								      : sharedType.getAttribute('__type');
							 | 
						||
| 
								 | 
							
								  invariant(type != null, 'Expected shared type to include type attribute');
							 | 
						||
| 
								 | 
							
								  return type;
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								export function $getOrInitCollabNodeFromSharedType(
							 | 
						||
| 
								 | 
							
								  binding: Binding,
							 | 
						||
| 
								 | 
							
								  sharedType: XmlText | YMap<unknown> | XmlElement,
							 | 
						||
| 
								 | 
							
								  parent?: CollabElementNode,
							 | 
						||
| 
								 | 
							
								):
							 | 
						||
| 
								 | 
							
								  | CollabElementNode
							 | 
						||
| 
								 | 
							
								  | CollabTextNode
							 | 
						||
| 
								 | 
							
								  | CollabLineBreakNode
							 | 
						||
| 
								 | 
							
								  | CollabDecoratorNode {
							 | 
						||
| 
								 | 
							
								  const collabNode = sharedType._collabNode;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (collabNode === undefined) {
							 | 
						||
| 
								 | 
							
								    const registeredNodes = binding.editor._nodes;
							 | 
						||
| 
								 | 
							
								    const type = getNodeTypeFromSharedType(sharedType);
							 | 
						||
| 
								 | 
							
								    const nodeInfo = registeredNodes.get(type);
							 | 
						||
| 
								 | 
							
								    invariant(nodeInfo !== undefined, 'Node %s is not registered', type);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    const sharedParent = sharedType.parent;
							 | 
						||
| 
								 | 
							
								    const targetParent =
							 | 
						||
| 
								 | 
							
								      parent === undefined && sharedParent !== null
							 | 
						||
| 
								 | 
							
								        ? $getOrInitCollabNodeFromSharedType(
							 | 
						||
| 
								 | 
							
								            binding,
							 | 
						||
| 
								 | 
							
								            sharedParent as XmlText | YMap<unknown> | XmlElement,
							 | 
						||
| 
								 | 
							
								          )
							 | 
						||
| 
								 | 
							
								        : parent || null;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    invariant(
							 | 
						||
| 
								 | 
							
								      targetParent instanceof CollabElementNode,
							 | 
						||
| 
								 | 
							
								      'Expected parent to be a collab element node',
							 | 
						||
| 
								 | 
							
								    );
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (sharedType instanceof XmlText) {
							 | 
						||
| 
								 | 
							
								      return $createCollabElementNode(sharedType, targetParent, type);
							 | 
						||
| 
								 | 
							
								    } else if (sharedType instanceof YMap) {
							 | 
						||
| 
								 | 
							
								      if (type === 'linebreak') {
							 | 
						||
| 
								 | 
							
								        return $createCollabLineBreakNode(sharedType, targetParent);
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								      return $createCollabTextNode(sharedType, '', targetParent, type);
							 | 
						||
| 
								 | 
							
								    } else if (sharedType instanceof XmlElement) {
							 | 
						||
| 
								 | 
							
								      return $createCollabDecoratorNode(sharedType, targetParent, type);
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  return collabNode;
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								export function createLexicalNodeFromCollabNode(
							 | 
						||
| 
								 | 
							
								  binding: Binding,
							 | 
						||
| 
								 | 
							
								  collabNode:
							 | 
						||
| 
								 | 
							
								    | CollabElementNode
							 | 
						||
| 
								 | 
							
								    | CollabTextNode
							 | 
						||
| 
								 | 
							
								    | CollabDecoratorNode
							 | 
						||
| 
								 | 
							
								    | CollabLineBreakNode,
							 | 
						||
| 
								 | 
							
								  parentKey: NodeKey,
							 | 
						||
| 
								 | 
							
								): LexicalNode {
							 | 
						||
| 
								 | 
							
								  const type = collabNode.getType();
							 | 
						||
| 
								 | 
							
								  const registeredNodes = binding.editor._nodes;
							 | 
						||
| 
								 | 
							
								  const nodeInfo = registeredNodes.get(type);
							 | 
						||
| 
								 | 
							
								  invariant(nodeInfo !== undefined, 'Node %s is not registered', type);
							 | 
						||
| 
								 | 
							
								  const lexicalNode:
							 | 
						||
| 
								 | 
							
								    | DecoratorNode<unknown>
							 | 
						||
| 
								 | 
							
								    | TextNode
							 | 
						||
| 
								 | 
							
								    | ElementNode
							 | 
						||
| 
								 | 
							
								    | LexicalNode = new nodeInfo.klass();
							 | 
						||
| 
								 | 
							
								  lexicalNode.__parent = parentKey;
							 | 
						||
| 
								 | 
							
								  collabNode._key = lexicalNode.__key;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (collabNode instanceof CollabElementNode) {
							 | 
						||
| 
								 | 
							
								    const xmlText = collabNode._xmlText;
							 | 
						||
| 
								 | 
							
								    collabNode.syncPropertiesFromYjs(binding, null);
							 | 
						||
| 
								 | 
							
								    collabNode.applyChildrenYjsDelta(binding, xmlText.toDelta());
							 | 
						||
| 
								 | 
							
								    collabNode.syncChildrenFromYjs(binding);
							 | 
						||
| 
								 | 
							
								  } else if (collabNode instanceof CollabTextNode) {
							 | 
						||
| 
								 | 
							
								    collabNode.syncPropertiesAndTextFromYjs(binding, null);
							 | 
						||
| 
								 | 
							
								  } else if (collabNode instanceof CollabDecoratorNode) {
							 | 
						||
| 
								 | 
							
								    collabNode.syncPropertiesFromYjs(binding, null);
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  binding.collabNodeMap.set(lexicalNode.__key, collabNode);
							 | 
						||
| 
								 | 
							
								  return lexicalNode;
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								export function syncPropertiesFromYjs(
							 | 
						||
| 
								 | 
							
								  binding: Binding,
							 | 
						||
| 
								 | 
							
								  sharedType: XmlText | YMap<unknown> | XmlElement,
							 | 
						||
| 
								 | 
							
								  lexicalNode: LexicalNode,
							 | 
						||
| 
								 | 
							
								  keysChanged: null | Set<string>,
							 | 
						||
| 
								 | 
							
								): void {
							 | 
						||
| 
								 | 
							
								  const properties =
							 | 
						||
| 
								 | 
							
								    keysChanged === null
							 | 
						||
| 
								 | 
							
								      ? sharedType instanceof YMap
							 | 
						||
| 
								 | 
							
								        ? Array.from(sharedType.keys())
							 | 
						||
| 
								 | 
							
								        : Object.keys(sharedType.getAttributes())
							 | 
						||
| 
								 | 
							
								      : Array.from(keysChanged);
							 | 
						||
| 
								 | 
							
								  let writableNode;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  for (let i = 0; i < properties.length; i++) {
							 | 
						||
| 
								 | 
							
								    const property = properties[i];
							 | 
						||
| 
								 | 
							
								    if (isExcludedProperty(property, lexicalNode, binding)) {
							 | 
						||
| 
								 | 
							
								      continue;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								    // eslint-disable-next-line @typescript-eslint/no-explicit-any
							 | 
						||
| 
								 | 
							
								    const prevValue = (lexicalNode as any)[property];
							 | 
						||
| 
								 | 
							
								    let nextValue =
							 | 
						||
| 
								 | 
							
								      sharedType instanceof YMap
							 | 
						||
| 
								 | 
							
								        ? sharedType.get(property)
							 | 
						||
| 
								 | 
							
								        : sharedType.getAttribute(property);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (prevValue !== nextValue) {
							 | 
						||
| 
								 | 
							
								      if (nextValue instanceof Doc) {
							 | 
						||
| 
								 | 
							
								        const yjsDocMap = binding.docMap;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        if (prevValue instanceof Doc) {
							 | 
						||
| 
								 | 
							
								          yjsDocMap.delete(prevValue.guid);
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        const nestedEditor = createEditor();
							 | 
						||
| 
								 | 
							
								        const key = nextValue.guid;
							 | 
						||
| 
								 | 
							
								        nestedEditor._key = key;
							 | 
						||
| 
								 | 
							
								        yjsDocMap.set(key, nextValue);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        nextValue = nestedEditor;
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      if (writableNode === undefined) {
							 | 
						||
| 
								 | 
							
								        writableNode = lexicalNode.getWritable();
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      writableNode[property as keyof typeof writableNode] = nextValue;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								export function syncPropertiesFromLexical(
							 | 
						||
| 
								 | 
							
								  binding: Binding,
							 | 
						||
| 
								 | 
							
								  sharedType: XmlText | YMap<unknown> | XmlElement,
							 | 
						||
| 
								 | 
							
								  prevLexicalNode: null | LexicalNode,
							 | 
						||
| 
								 | 
							
								  nextLexicalNode: LexicalNode,
							 | 
						||
| 
								 | 
							
								): void {
							 | 
						||
| 
								 | 
							
								  const type = nextLexicalNode.__type;
							 | 
						||
| 
								 | 
							
								  const nodeProperties = binding.nodeProperties;
							 | 
						||
| 
								 | 
							
								  let properties = nodeProperties.get(type);
							 | 
						||
| 
								 | 
							
								  if (properties === undefined) {
							 | 
						||
| 
								 | 
							
								    properties = Object.keys(nextLexicalNode).filter((property) => {
							 | 
						||
| 
								 | 
							
								      return !isExcludedProperty(property, nextLexicalNode, binding);
							 | 
						||
| 
								 | 
							
								    });
							 | 
						||
| 
								 | 
							
								    nodeProperties.set(type, properties);
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  const EditorClass = binding.editor.constructor;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  for (let i = 0; i < properties.length; i++) {
							 | 
						||
| 
								 | 
							
								    const property = properties[i];
							 | 
						||
| 
								 | 
							
								    const prevValue =
							 | 
						||
| 
								 | 
							
								      // eslint-disable-next-line @typescript-eslint/no-explicit-any
							 | 
						||
| 
								 | 
							
								      prevLexicalNode === null ? undefined : (prevLexicalNode as any)[property];
							 | 
						||
| 
								 | 
							
								    // eslint-disable-next-line @typescript-eslint/no-explicit-any
							 | 
						||
| 
								 | 
							
								    let nextValue = (nextLexicalNode as any)[property];
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (prevValue !== nextValue) {
							 | 
						||
| 
								 | 
							
								      if (nextValue instanceof EditorClass) {
							 | 
						||
| 
								 | 
							
								        const yjsDocMap = binding.docMap;
							 | 
						||
| 
								 | 
							
								        let prevDoc;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        if (prevValue instanceof EditorClass) {
							 | 
						||
| 
								 | 
							
								          const prevKey = prevValue._key;
							 | 
						||
| 
								 | 
							
								          prevDoc = yjsDocMap.get(prevKey);
							 | 
						||
| 
								 | 
							
								          yjsDocMap.delete(prevKey);
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        // If we already have a document, use it.
							 | 
						||
| 
								 | 
							
								        const doc = prevDoc || new Doc();
							 | 
						||
| 
								 | 
							
								        const key = doc.guid;
							 | 
						||
| 
								 | 
							
								        nextValue._key = key;
							 | 
						||
| 
								 | 
							
								        yjsDocMap.set(key, doc);
							 | 
						||
| 
								 | 
							
								        nextValue = doc;
							 | 
						||
| 
								 | 
							
								        // Mark the node dirty as we've assigned a new key to it
							 | 
						||
| 
								 | 
							
								        binding.editor.update(() => {
							 | 
						||
| 
								 | 
							
								          nextLexicalNode.markDirty();
							 | 
						||
| 
								 | 
							
								        });
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      if (sharedType instanceof YMap) {
							 | 
						||
| 
								 | 
							
								        sharedType.set(property, nextValue);
							 | 
						||
| 
								 | 
							
								      } else {
							 | 
						||
| 
								 | 
							
								        sharedType.setAttribute(property, nextValue);
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								export function spliceString(
							 | 
						||
| 
								 | 
							
								  str: string,
							 | 
						||
| 
								 | 
							
								  index: number,
							 | 
						||
| 
								 | 
							
								  delCount: number,
							 | 
						||
| 
								 | 
							
								  newText: string,
							 | 
						||
| 
								 | 
							
								): string {
							 | 
						||
| 
								 | 
							
								  return str.slice(0, index) + newText + str.slice(index + delCount);
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								export function getPositionFromElementAndOffset(
							 | 
						||
| 
								 | 
							
								  node: CollabElementNode,
							 | 
						||
| 
								 | 
							
								  offset: number,
							 | 
						||
| 
								 | 
							
								  boundaryIsEdge: boolean,
							 | 
						||
| 
								 | 
							
								): {
							 | 
						||
| 
								 | 
							
								  length: number;
							 | 
						||
| 
								 | 
							
								  node:
							 | 
						||
| 
								 | 
							
								    | CollabElementNode
							 | 
						||
| 
								 | 
							
								    | CollabTextNode
							 | 
						||
| 
								 | 
							
								    | CollabDecoratorNode
							 | 
						||
| 
								 | 
							
								    | CollabLineBreakNode
							 | 
						||
| 
								 | 
							
								    | null;
							 | 
						||
| 
								 | 
							
								  nodeIndex: number;
							 | 
						||
| 
								 | 
							
								  offset: number;
							 | 
						||
| 
								 | 
							
								} {
							 | 
						||
| 
								 | 
							
								  let index = 0;
							 | 
						||
| 
								 | 
							
								  let i = 0;
							 | 
						||
| 
								 | 
							
								  const children = node._children;
							 | 
						||
| 
								 | 
							
								  const childrenLength = children.length;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  for (; i < childrenLength; i++) {
							 | 
						||
| 
								 | 
							
								    const child = children[i];
							 | 
						||
| 
								 | 
							
								    const childOffset = index;
							 | 
						||
| 
								 | 
							
								    const size = child.getSize();
							 | 
						||
| 
								 | 
							
								    index += size;
							 | 
						||
| 
								 | 
							
								    const exceedsBoundary = boundaryIsEdge ? index >= offset : index > offset;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (exceedsBoundary && child instanceof CollabTextNode) {
							 | 
						||
| 
								 | 
							
								      let textOffset = offset - childOffset - 1;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      if (textOffset < 0) {
							 | 
						||
| 
								 | 
							
								        textOffset = 0;
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      const diffLength = index - offset;
							 | 
						||
| 
								 | 
							
								      return {
							 | 
						||
| 
								 | 
							
								        length: diffLength,
							 | 
						||
| 
								 | 
							
								        node: child,
							 | 
						||
| 
								 | 
							
								        nodeIndex: i,
							 | 
						||
| 
								 | 
							
								        offset: textOffset,
							 | 
						||
| 
								 | 
							
								      };
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (index > offset) {
							 | 
						||
| 
								 | 
							
								      return {
							 | 
						||
| 
								 | 
							
								        length: 0,
							 | 
						||
| 
								 | 
							
								        node: child,
							 | 
						||
| 
								 | 
							
								        nodeIndex: i,
							 | 
						||
| 
								 | 
							
								        offset: childOffset,
							 | 
						||
| 
								 | 
							
								      };
							 | 
						||
| 
								 | 
							
								    } else if (i === childrenLength - 1) {
							 | 
						||
| 
								 | 
							
								      return {
							 | 
						||
| 
								 | 
							
								        length: 0,
							 | 
						||
| 
								 | 
							
								        node: null,
							 | 
						||
| 
								 | 
							
								        nodeIndex: i + 1,
							 | 
						||
| 
								 | 
							
								        offset: childOffset + 1,
							 | 
						||
| 
								 | 
							
								      };
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  return {
							 | 
						||
| 
								 | 
							
								    length: 0,
							 | 
						||
| 
								 | 
							
								    node: null,
							 | 
						||
| 
								 | 
							
								    nodeIndex: 0,
							 | 
						||
| 
								 | 
							
								    offset: 0,
							 | 
						||
| 
								 | 
							
								  };
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								export function doesSelectionNeedRecovering(
							 | 
						||
| 
								 | 
							
								  selection: RangeSelection,
							 | 
						||
| 
								 | 
							
								): boolean {
							 | 
						||
| 
								 | 
							
								  const anchor = selection.anchor;
							 | 
						||
| 
								 | 
							
								  const focus = selection.focus;
							 | 
						||
| 
								 | 
							
								  let recoveryNeeded = false;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  try {
							 | 
						||
| 
								 | 
							
								    const anchorNode = anchor.getNode();
							 | 
						||
| 
								 | 
							
								    const focusNode = focus.getNode();
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (
							 | 
						||
| 
								 | 
							
								      // We might have removed a node that no longer exists
							 | 
						||
| 
								 | 
							
								      !anchorNode.isAttached() ||
							 | 
						||
| 
								 | 
							
								      !focusNode.isAttached() ||
							 | 
						||
| 
								 | 
							
								      // If we've split a node, then the offset might not be right
							 | 
						||
| 
								 | 
							
								      ($isTextNode(anchorNode) &&
							 | 
						||
| 
								 | 
							
								        anchor.offset > anchorNode.getTextContentSize()) ||
							 | 
						||
| 
								 | 
							
								      ($isTextNode(focusNode) && focus.offset > focusNode.getTextContentSize())
							 | 
						||
| 
								 | 
							
								    ) {
							 | 
						||
| 
								 | 
							
								      recoveryNeeded = true;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  } catch (e) {
							 | 
						||
| 
								 | 
							
								    // Sometimes checking nor a node via getNode might trigger
							 | 
						||
| 
								 | 
							
								    // an error, so we need recovery then too.
							 | 
						||
| 
								 | 
							
								    recoveryNeeded = true;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  return recoveryNeeded;
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								export function syncWithTransaction(binding: Binding, fn: () => void): void {
							 | 
						||
| 
								 | 
							
								  binding.doc.transact(fn, binding);
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								export function removeFromParent(node: LexicalNode): void {
							 | 
						||
| 
								 | 
							
								  const oldParent = node.getParent();
							 | 
						||
| 
								 | 
							
								  if (oldParent !== null) {
							 | 
						||
| 
								 | 
							
								    const writableNode = node.getWritable();
							 | 
						||
| 
								 | 
							
								    const writableParent = oldParent.getWritable();
							 | 
						||
| 
								 | 
							
								    const prevSibling = node.getPreviousSibling();
							 | 
						||
| 
								 | 
							
								    const nextSibling = node.getNextSibling();
							 | 
						||
| 
								 | 
							
								    // TODO: this function duplicates a bunch of operations, can be simplified.
							 | 
						||
| 
								 | 
							
								    if (prevSibling === null) {
							 | 
						||
| 
								 | 
							
								      if (nextSibling !== null) {
							 | 
						||
| 
								 | 
							
								        const writableNextSibling = nextSibling.getWritable();
							 | 
						||
| 
								 | 
							
								        writableParent.__first = nextSibling.__key;
							 | 
						||
| 
								 | 
							
								        writableNextSibling.__prev = null;
							 | 
						||
| 
								 | 
							
								      } else {
							 | 
						||
| 
								 | 
							
								        writableParent.__first = null;
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    } else {
							 | 
						||
| 
								 | 
							
								      const writablePrevSibling = prevSibling.getWritable();
							 | 
						||
| 
								 | 
							
								      if (nextSibling !== null) {
							 | 
						||
| 
								 | 
							
								        const writableNextSibling = nextSibling.getWritable();
							 | 
						||
| 
								 | 
							
								        writableNextSibling.__prev = writablePrevSibling.__key;
							 | 
						||
| 
								 | 
							
								        writablePrevSibling.__next = writableNextSibling.__key;
							 | 
						||
| 
								 | 
							
								      } else {
							 | 
						||
| 
								 | 
							
								        writablePrevSibling.__next = null;
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								      writableNode.__prev = null;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								    if (nextSibling === null) {
							 | 
						||
| 
								 | 
							
								      if (prevSibling !== null) {
							 | 
						||
| 
								 | 
							
								        const writablePrevSibling = prevSibling.getWritable();
							 | 
						||
| 
								 | 
							
								        writableParent.__last = prevSibling.__key;
							 | 
						||
| 
								 | 
							
								        writablePrevSibling.__next = null;
							 | 
						||
| 
								 | 
							
								      } else {
							 | 
						||
| 
								 | 
							
								        writableParent.__last = null;
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    } else {
							 | 
						||
| 
								 | 
							
								      const writableNextSibling = nextSibling.getWritable();
							 | 
						||
| 
								 | 
							
								      if (prevSibling !== null) {
							 | 
						||
| 
								 | 
							
								        const writablePrevSibling = prevSibling.getWritable();
							 | 
						||
| 
								 | 
							
								        writablePrevSibling.__next = writableNextSibling.__key;
							 | 
						||
| 
								 | 
							
								        writableNextSibling.__prev = writablePrevSibling.__key;
							 | 
						||
| 
								 | 
							
								      } else {
							 | 
						||
| 
								 | 
							
								        writableNextSibling.__prev = null;
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								      writableNode.__next = null;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								    writableParent.__size--;
							 | 
						||
| 
								 | 
							
								    writableNode.__parent = null;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								export function $moveSelectionToPreviousNode(
							 | 
						||
| 
								 | 
							
								  anchorNodeKey: string,
							 | 
						||
| 
								 | 
							
								  currentEditorState: EditorState,
							 | 
						||
| 
								 | 
							
								) {
							 | 
						||
| 
								 | 
							
								  const anchorNode = currentEditorState._nodeMap.get(anchorNodeKey);
							 | 
						||
| 
								 | 
							
								  if (!anchorNode) {
							 | 
						||
| 
								 | 
							
								    $getRoot().selectStart();
							 | 
						||
| 
								 | 
							
								    return;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								  // Get previous node
							 | 
						||
| 
								 | 
							
								  const prevNodeKey = anchorNode.__prev;
							 | 
						||
| 
								 | 
							
								  let prevNode: ElementNode | null = null;
							 | 
						||
| 
								 | 
							
								  if (prevNodeKey) {
							 | 
						||
| 
								 | 
							
								    prevNode = $getNodeByKey(prevNodeKey);
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // If previous node not found, get parent node
							 | 
						||
| 
								 | 
							
								  if (prevNode === null && anchorNode.__parent !== null) {
							 | 
						||
| 
								 | 
							
								    prevNode = $getNodeByKey(anchorNode.__parent);
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								  if (prevNode === null) {
							 | 
						||
| 
								 | 
							
								    $getRoot().selectStart();
							 | 
						||
| 
								 | 
							
								    return;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (prevNode !== null && prevNode.isAttached()) {
							 | 
						||
| 
								 | 
							
								    prevNode.selectEnd();
							 | 
						||
| 
								 | 
							
								    return;
							 | 
						||
| 
								 | 
							
								  } else {
							 | 
						||
| 
								 | 
							
								    // If the found node is also deleted, select the next one
							 | 
						||
| 
								 | 
							
								    $moveSelectionToPreviousNode(prevNode.__key, currentEditorState);
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								}
							 |