mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-12-14 15:13:37 +08:00
22d078b47f
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.
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;
|
|
}
|