179 lines
		
	
	
		
			4.1 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
		
		
			
		
	
	
			179 lines
		
	
	
		
			4.1 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 {CollabElementNode} from './CollabElementNode';
							 | 
						||
| 
								 | 
							
								import type {NodeKey, NodeMap, TextNode} from 'lexical';
							 | 
						||
| 
								 | 
							
								import type {Map as YMap} from 'yjs';
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								import {
							 | 
						||
| 
								 | 
							
								  $getNodeByKey,
							 | 
						||
| 
								 | 
							
								  $getSelection,
							 | 
						||
| 
								 | 
							
								  $isRangeSelection,
							 | 
						||
| 
								 | 
							
								  $isTextNode,
							 | 
						||
| 
								 | 
							
								} from 'lexical';
							 | 
						||
| 
								 | 
							
								import invariant from 'lexical/shared/invariant';
							 | 
						||
| 
								 | 
							
								import simpleDiffWithCursor from 'lexical/shared/simpleDiffWithCursor';
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								import {syncPropertiesFromLexical, syncPropertiesFromYjs} from './Utils';
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								function $diffTextContentAndApplyDelta(
							 | 
						||
| 
								 | 
							
								  collabNode: CollabTextNode,
							 | 
						||
| 
								 | 
							
								  key: NodeKey,
							 | 
						||
| 
								 | 
							
								  prevText: string,
							 | 
						||
| 
								 | 
							
								  nextText: string,
							 | 
						||
| 
								 | 
							
								): void {
							 | 
						||
| 
								 | 
							
								  const selection = $getSelection();
							 | 
						||
| 
								 | 
							
								  let cursorOffset = nextText.length;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if ($isRangeSelection(selection) && selection.isCollapsed()) {
							 | 
						||
| 
								 | 
							
								    const anchor = selection.anchor;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (anchor.key === key) {
							 | 
						||
| 
								 | 
							
								      cursorOffset = anchor.offset;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  const diff = simpleDiffWithCursor(prevText, nextText, cursorOffset);
							 | 
						||
| 
								 | 
							
								  collabNode.spliceText(diff.index, diff.remove, diff.insert);
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								export class CollabTextNode {
							 | 
						||
| 
								 | 
							
								  _map: YMap<unknown>;
							 | 
						||
| 
								 | 
							
								  _key: NodeKey;
							 | 
						||
| 
								 | 
							
								  _parent: CollabElementNode;
							 | 
						||
| 
								 | 
							
								  _text: string;
							 | 
						||
| 
								 | 
							
								  _type: string;
							 | 
						||
| 
								 | 
							
								  _normalized: boolean;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  constructor(
							 | 
						||
| 
								 | 
							
								    map: YMap<unknown>,
							 | 
						||
| 
								 | 
							
								    text: string,
							 | 
						||
| 
								 | 
							
								    parent: CollabElementNode,
							 | 
						||
| 
								 | 
							
								    type: string,
							 | 
						||
| 
								 | 
							
								  ) {
							 | 
						||
| 
								 | 
							
								    this._key = '';
							 | 
						||
| 
								 | 
							
								    this._map = map;
							 | 
						||
| 
								 | 
							
								    this._parent = parent;
							 | 
						||
| 
								 | 
							
								    this._text = text;
							 | 
						||
| 
								 | 
							
								    this._type = type;
							 | 
						||
| 
								 | 
							
								    this._normalized = false;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  getPrevNode(nodeMap: null | NodeMap): null | TextNode {
							 | 
						||
| 
								 | 
							
								    if (nodeMap === null) {
							 | 
						||
| 
								 | 
							
								      return null;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    const node = nodeMap.get(this._key);
							 | 
						||
| 
								 | 
							
								    return $isTextNode(node) ? node : null;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  getNode(): null | TextNode {
							 | 
						||
| 
								 | 
							
								    const node = $getNodeByKey(this._key);
							 | 
						||
| 
								 | 
							
								    return $isTextNode(node) ? node : null;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  getSharedType(): YMap<unknown> {
							 | 
						||
| 
								 | 
							
								    return this._map;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  getType(): string {
							 | 
						||
| 
								 | 
							
								    return this._type;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  getKey(): NodeKey {
							 | 
						||
| 
								 | 
							
								    return this._key;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  getSize(): number {
							 | 
						||
| 
								 | 
							
								    return this._text.length + (this._normalized ? 0 : 1);
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  getOffset(): number {
							 | 
						||
| 
								 | 
							
								    const collabElementNode = this._parent;
							 | 
						||
| 
								 | 
							
								    return collabElementNode.getChildOffset(this);
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  spliceText(index: number, delCount: number, newText: string): void {
							 | 
						||
| 
								 | 
							
								    const collabElementNode = this._parent;
							 | 
						||
| 
								 | 
							
								    const xmlText = collabElementNode._xmlText;
							 | 
						||
| 
								 | 
							
								    const offset = this.getOffset() + 1 + index;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (delCount !== 0) {
							 | 
						||
| 
								 | 
							
								      xmlText.delete(offset, delCount);
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (newText !== '') {
							 | 
						||
| 
								 | 
							
								      xmlText.insert(offset, newText);
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  syncPropertiesAndTextFromLexical(
							 | 
						||
| 
								 | 
							
								    binding: Binding,
							 | 
						||
| 
								 | 
							
								    nextLexicalNode: TextNode,
							 | 
						||
| 
								 | 
							
								    prevNodeMap: null | NodeMap,
							 | 
						||
| 
								 | 
							
								  ): void {
							 | 
						||
| 
								 | 
							
								    const prevLexicalNode = this.getPrevNode(prevNodeMap);
							 | 
						||
| 
								 | 
							
								    const nextText = nextLexicalNode.__text;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    syncPropertiesFromLexical(
							 | 
						||
| 
								 | 
							
								      binding,
							 | 
						||
| 
								 | 
							
								      this._map,
							 | 
						||
| 
								 | 
							
								      prevLexicalNode,
							 | 
						||
| 
								 | 
							
								      nextLexicalNode,
							 | 
						||
| 
								 | 
							
								    );
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (prevLexicalNode !== null) {
							 | 
						||
| 
								 | 
							
								      const prevText = prevLexicalNode.__text;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      if (prevText !== nextText) {
							 | 
						||
| 
								 | 
							
								        const key = nextLexicalNode.__key;
							 | 
						||
| 
								 | 
							
								        $diffTextContentAndApplyDelta(this, key, prevText, nextText);
							 | 
						||
| 
								 | 
							
								        this._text = nextText;
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  syncPropertiesAndTextFromYjs(
							 | 
						||
| 
								 | 
							
								    binding: Binding,
							 | 
						||
| 
								 | 
							
								    keysChanged: null | Set<string>,
							 | 
						||
| 
								 | 
							
								  ): void {
							 | 
						||
| 
								 | 
							
								    const lexicalNode = this.getNode();
							 | 
						||
| 
								 | 
							
								    invariant(
							 | 
						||
| 
								 | 
							
								      lexicalNode !== null,
							 | 
						||
| 
								 | 
							
								      'syncPropertiesAndTextFromYjs: could not find decorator node',
							 | 
						||
| 
								 | 
							
								    );
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    syncPropertiesFromYjs(binding, this._map, lexicalNode, keysChanged);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    const collabText = this._text;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (lexicalNode.__text !== collabText) {
							 | 
						||
| 
								 | 
							
								      const writable = lexicalNode.getWritable();
							 | 
						||
| 
								 | 
							
								      writable.__text = collabText;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  destroy(binding: Binding): void {
							 | 
						||
| 
								 | 
							
								    const collabNodeMap = binding.collabNodeMap;
							 | 
						||
| 
								 | 
							
								    collabNodeMap.delete(this._key);
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								export function $createCollabTextNode(
							 | 
						||
| 
								 | 
							
								  map: YMap<unknown>,
							 | 
						||
| 
								 | 
							
								  text: string,
							 | 
						||
| 
								 | 
							
								  parent: CollabElementNode,
							 | 
						||
| 
								 | 
							
								  type: string,
							 | 
						||
| 
								 | 
							
								): CollabTextNode {
							 | 
						||
| 
								 | 
							
								  const collabNode = new CollabTextNode(map, text, parent, type);
							 | 
						||
| 
								 | 
							
								  map._collabNode = collabNode;
							 | 
						||
| 
								 | 
							
								  return collabNode;
							 | 
						||
| 
								 | 
							
								}
							 |