UNPKG

@wangeditor-next/yjs

Version:
1 lines 93.8 kB
{"version":3,"file":"index.mjs","sources":["../src/utils/object.ts","../src/utils/delta.ts","../src/utils/location.ts","../src/utils/yjs.ts","../src/utils/position.ts","../src/utils/slate.ts","../src/utils/convert.ts","../src/applyToSlate/textEvent.ts","../src/applyToSlate/index.ts","../src/utils/clone.ts","../src/applyToYjs/node/index.ts","../src/applyToYjs/node/insertNode.ts","../src/applyToYjs/node/removeNode.ts","../src/applyToYjs/node/setNode.ts","../src/applyToYjs/node/mergeNode.ts","../src/applyToYjs/node/moveNode.ts","../src/applyToYjs/node/splitNode.ts","../src/applyToYjs/text/index.ts","../src/applyToYjs/index.ts","../src/applyToYjs/text/insertText.ts","../src/applyToYjs/text/removeText.ts","../src/plugins/withYjs.ts","../src/plugins/withCursors.ts","../src/plugins/withYHistory.ts"],"sourcesContent":["type InspectableObject = Record<string | number | symbol, unknown>\n\nfunction isObject(o: unknown): o is InspectableObject {\n return Object.prototype.toString.call(o) === '[object Object]'\n}\n\nexport function isPlainObject(o: unknown): o is InspectableObject {\n if (!isObject(o)) {\n return false\n }\n\n // If has modified constructor\n const ctor = o.constructor\n\n if (ctor === undefined) {\n return true\n }\n\n // If has modified prototype\n const prot = ctor.prototype\n\n if (isObject(prot) === false) {\n return false\n }\n\n // If constructor does not have an Object-specific method\n\n if (Object.prototype.hasOwnProperty.call(prot, 'isPrototypeOfOf')) {\n return false\n }\n\n // Most likely a plain Object\n return true\n}\n\n// Slates deep equality function: https://github.com/ianstormtaylor/slate/blob/68aff89e892fe15a16314398ff052ade6068900b/packages/slate/src/utils/deep-equal.ts#L13\n// We have to match slates deepEquals behavior to merge insert deltas in the same way slate does.\nexport function deepEquals(node: InspectableObject, another: InspectableObject): boolean {\n // eslint-disable-next-line guard-for-in\n for (const key in node) {\n const a = node[key]\n const b = another[key]\n\n if (isPlainObject(a) && isPlainObject(b)) {\n if (!deepEquals(a, b)) {\n return false\n }\n } else if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) { return false }\n for (let i = 0; i < a.length; i += 1) {\n if (a[i] !== b[i]) {\n return false\n }\n }\n } else if (a !== b) {\n return false\n }\n }\n\n for (const key in another) {\n if (node[key] === undefined && another[key] !== undefined) {\n return false\n }\n }\n\n return true\n}\n\nexport function pick<TObj extends Object, TKeys extends keyof TObj>(\n obj: TObj,\n ...keys: TKeys[]\n): Pick<TObj, TKeys> {\n return Object.fromEntries(\n Object.entries(obj).filter(([key]) => keys.includes(key as TKeys)),\n ) as Pick<TObj, TKeys>\n}\n\nexport function omit<TObj extends Object, TKeys extends keyof TObj>(\n obj: TObj,\n ...keys: TKeys[]\n): Omit<TObj, TKeys> {\n return Object.fromEntries(\n Object.entries(obj).filter(([key]) => !keys.includes(key as TKeys)),\n ) as Omit<TObj, TKeys>\n}\n\nexport function omitNullEntries<TObj extends Object>(\n obj: TObj,\n): {\n [K in keyof TObj]: TObj[K] extends null ? never : K\n} {\n return Object.fromEntries(Object.entries(obj).filter(([, value]) => value !== null)) as {\n [K in keyof TObj]: TObj[K] extends null ? never : K\n }\n}\n","import * as Y from 'yjs'\n\nimport { DeltaInsert, InsertDelta } from '../module/custom-types'\nimport { deepEquals } from './object'\n\nexport function normalizeInsertDelta(delta: InsertDelta): InsertDelta {\n const normalized: InsertDelta = []\n\n for (const element of delta) {\n if (typeof element.insert === 'string' && element.insert.length === 0) {\n continue\n }\n\n const prev = normalized[normalized.length - 1]\n\n if (!prev || typeof prev.insert !== 'string' || typeof element.insert !== 'string') {\n normalized.push(element)\n continue\n }\n\n const merge = prev.attributes === element.attributes\n || (!prev.attributes === !element.attributes\n && deepEquals(prev.attributes ?? {}, element.attributes ?? {}))\n\n if (merge) {\n prev.insert += element.insert\n continue\n }\n\n normalized.push(element)\n }\n\n return normalized\n}\n\nexport function yTextToInsertDelta(yText: Y.XmlText): InsertDelta {\n return normalizeInsertDelta(yText.toDelta())\n}\n\nexport function getInsertLength({ insert }: DeltaInsert): number {\n return typeof insert === 'string' ? insert.length : 1\n}\n\nexport function getInsertDeltaLength(delta: InsertDelta): number {\n return delta.reduce((curr, element) => curr + getInsertLength(element), 0)\n}\n\nexport function sliceInsertDelta(delta: InsertDelta, start: number, length: number): InsertDelta {\n if (length < 1) {\n return []\n }\n\n let currentOffset = 0\n const sliced: InsertDelta = []\n const end = start + length\n\n for (let i = 0; i < delta.length; i += 1) {\n if (currentOffset >= end) {\n break\n }\n\n const element = delta[i]\n const elementLength = getInsertLength(element)\n\n if (currentOffset + elementLength <= start) {\n currentOffset += elementLength\n continue\n }\n\n if (typeof element.insert !== 'string') {\n currentOffset += elementLength\n sliced.push(element)\n continue\n }\n\n const startOffset = Math.max(0, start - currentOffset)\n const endOffset = Math.min(elementLength, elementLength - (currentOffset + elementLength - end))\n\n sliced.push({\n ...element,\n insert: element.insert.slice(startOffset, endOffset),\n })\n currentOffset += elementLength\n }\n\n return sliced\n}\n","import {\n Element, Node, Path, Text,\n} from 'slate'\nimport * as Y from 'yjs'\n\nimport { YTarget } from '../module/custom-types'\nimport { sliceInsertDelta, yTextToInsertDelta } from './delta'\n\nexport function getSlateNodeYLength(node: Node | undefined): number {\n if (!node) {\n return 0\n }\n\n return Text.isText(node) ? node.text.length : 1\n}\n\nexport function slatePathOffsetToYOffset(element: Element, pathOffset: number) {\n return element.children\n .slice(0, pathOffset)\n .reduce((yOffset, node) => yOffset + getSlateNodeYLength(node), 0)\n}\n\nexport function getYTarget(yRoot: Y.XmlText, slateRoot: Node, path: Path): YTarget {\n if (path.length === 0) {\n throw new Error('Path has to a have a length >= 1')\n }\n\n if (Text.isText(slateRoot)) {\n throw new Error('Cannot descent into slate text')\n }\n\n const [pathOffset, ...childPath] = path\n\n const yOffset = slatePathOffsetToYOffset(slateRoot as Element, pathOffset)\n const targetNode = slateRoot.children[pathOffset]\n\n const delta = yTextToInsertDelta(yRoot)\n const targetLength = getSlateNodeYLength(targetNode)\n\n const targetDelta = sliceInsertDelta(delta, yOffset, targetLength)\n\n if (targetDelta.length > 1) {\n throw new Error(\"Path doesn't match yText, yTarget spans multiple nodes\")\n }\n\n const yTarget = targetDelta[0]?.insert\n\n if (childPath.length > 0) {\n if (!(yTarget instanceof Y.XmlText)) {\n throw new Error(\"Path doesn't match yText, cannot descent into non-yText\")\n }\n\n return getYTarget(yTarget, targetNode, childPath)\n }\n\n return {\n yParent: yRoot,\n textRange: { start: yOffset, end: yOffset + targetLength },\n yTarget: yTarget instanceof Y.XmlText ? yTarget : undefined,\n slateParent: slateRoot,\n slateTarget: targetNode,\n targetDelta,\n }\n}\n\nexport function yOffsetToSlateOffsets(\n parent: Element,\n yOffset: number,\n opts: { assoc?: number; insert?: boolean } = {},\n): [number, number] {\n const { assoc = 0, insert = false } = opts\n\n let currentOffset = 0\n let lastNonEmptyPathOffset = 0\n\n for (let pathOffset = 0; pathOffset < parent.children.length; pathOffset += 1) {\n const child = parent.children[pathOffset]\n const nodeLength = Text.isText(child) ? child.text.length : 1\n\n if (nodeLength > 0) {\n lastNonEmptyPathOffset = pathOffset\n }\n\n const endOffset = currentOffset + nodeLength\n\n if (nodeLength > 0 && (assoc >= 0 ? endOffset > yOffset : endOffset >= yOffset)) {\n return [pathOffset, yOffset - currentOffset]\n }\n\n currentOffset += nodeLength\n }\n\n if (yOffset > currentOffset + (insert ? 1 : 0)) {\n throw new Error('yOffset out of bounds')\n }\n\n if (insert) {\n return [parent.children.length, 0]\n }\n\n const child = parent.children[lastNonEmptyPathOffset]\n const textOffset = Text.isText(child) ? child.text.length : 1\n\n return [lastNonEmptyPathOffset, textOffset]\n}\n\nexport function getSlatePath(sharedRoot: Y.XmlText, slateRoot: Node, yText: Y.XmlText): Path {\n const yNodePath = [yText]\n\n while (yNodePath[0] !== sharedRoot) {\n const { parent: yParent } = yNodePath[0]\n\n if (!yParent) {\n throw new Error(\"yText isn't a descendant of root element\")\n }\n\n if (!(yParent instanceof Y.XmlText)) {\n throw new Error('Unexpected y parent type')\n }\n\n yNodePath.unshift(yParent)\n }\n\n if (yNodePath.length < 2) {\n return []\n }\n\n let slateParent = slateRoot\n\n return yNodePath.reduce<Path>((path, yParent, idx) => {\n const yChild = yNodePath[idx + 1]\n\n if (!yChild) {\n return path\n }\n\n let yOffset = 0\n const currentDelta = yTextToInsertDelta(yParent)\n\n for (const element of currentDelta) {\n if (element.insert === yChild) {\n break\n }\n\n yOffset += typeof element.insert === 'string' ? element.insert.length : 1\n }\n\n if (Text.isText(slateParent)) {\n throw new Error('Cannot descent into slate text')\n }\n\n const [pathOffset] = yOffsetToSlateOffsets(slateParent as Element, yOffset)\n\n slateParent = slateParent.children[pathOffset]\n return path.concat(pathOffset)\n }, [])\n}\n","import * as Y from 'yjs'\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function assertDocumentAttachment<T extends Y.AbstractType<any>>(\n sharedType: T,\n): asserts sharedType is T & { doc: NonNullable<T['doc']> } {\n if (!sharedType.doc) {\n throw new Error(\"shared type isn't attached to a document\")\n }\n}\n","import {\n BasePoint, BaseRange, Element, Node, Text,\n} from 'slate'\nimport * as Y from 'yjs'\n\nimport { InsertDelta, RelativeRange, TextRange } from '../module/custom-types'\nimport { getInsertDeltaLength, yTextToInsertDelta } from './delta'\nimport { getSlatePath, getYTarget, yOffsetToSlateOffsets } from './location'\nimport { assertDocumentAttachment } from './yjs'\n\nexport const STORED_POSITION_PREFIX = '__slateYjsStoredPosition_'\n\nexport function slatePointToRelativePosition(\n sharedRoot: Y.XmlText,\n slateRoot: Node,\n point: BasePoint,\n): Y.RelativePosition {\n const { yTarget, yParent, textRange } = getYTarget(sharedRoot, slateRoot, point.path)\n\n if (yTarget) {\n throw new Error('Slate point points to a non-text element inside sharedRoot')\n }\n\n const index = textRange.start + point.offset\n\n return Y.createRelativePositionFromTypeIndex(yParent, index, index === textRange.end ? -1 : 0)\n}\n\nexport function absolutePositionToSlatePoint(\n sharedRoot: Y.XmlText,\n slateRoot: Node,\n { type, index, assoc }: Y.AbsolutePosition,\n): BasePoint | null {\n if (!(type instanceof Y.XmlText)) {\n throw new Error('Absolute position points to a non-XMLText')\n }\n\n const parentPath = getSlatePath(sharedRoot, slateRoot, type)\n const parent = Node.get(slateRoot, parentPath)\n\n if (Text.isText(parent) || !Element.isElement(parent)) {\n throw new Error(\"Absolute position doesn't match slateRoot, cannot descent into text and EditorRoot\")\n }\n\n const [pathOffset, textOffset] = yOffsetToSlateOffsets(parent, index, {\n assoc,\n })\n\n const target = parent.children[pathOffset]\n\n if (!Text.isText(target)) {\n return null\n }\n\n return { path: [...parentPath, pathOffset], offset: textOffset }\n}\n\nexport function relativePositionToSlatePoint(\n sharedRoot: Y.XmlText,\n slateRoot: Node,\n pos: Y.RelativePosition,\n): BasePoint | null {\n if (!sharedRoot.doc) {\n throw new Error(\"sharedRoot isn't attach to a yDoc\")\n }\n\n const absPos = Y.createAbsolutePositionFromRelativePosition(pos, sharedRoot.doc)\n\n return absPos && absolutePositionToSlatePoint(sharedRoot, slateRoot, absPos)\n}\n\nexport function getStoredPosition(sharedRoot: Y.XmlText, key: string): Y.RelativePosition | null {\n const rawPosition = sharedRoot.getAttribute(STORED_POSITION_PREFIX + key)\n\n if (!rawPosition) {\n return null\n }\n\n return Y.decodeRelativePosition(rawPosition)\n}\n\nexport function getStoredPositions(sharedRoot: Y.XmlText): Record<string, Y.RelativePosition> {\n return Object.fromEntries(\n Object.entries(sharedRoot.getAttributes())\n .filter(([key]) => key.startsWith(STORED_POSITION_PREFIX))\n .map(([key, position]) => [\n key.slice(STORED_POSITION_PREFIX.length),\n Y.createRelativePositionFromJSON(position),\n ]),\n )\n}\n\nfunction getStoredPositionsAbsolute(sharedRoot: Y.XmlText) {\n assertDocumentAttachment(sharedRoot)\n\n return Object.fromEntries(\n Object.entries(sharedRoot.getAttributes())\n .filter(([key]) => key.startsWith(STORED_POSITION_PREFIX))\n .map(\n ([key, position]) => [\n key.slice(STORED_POSITION_PREFIX.length),\n Y.createAbsolutePositionFromRelativePosition(\n Y.decodeRelativePosition(position),\n sharedRoot.doc,\n ),\n ] as const,\n )\n .filter(([, position]) => position),\n ) as Record<string, Y.AbsolutePosition>\n}\n\nexport function removeStoredPosition(sharedRoot: Y.XmlText, key: string) {\n sharedRoot.removeAttribute(STORED_POSITION_PREFIX + key)\n}\n\nexport function setStoredPosition(\n sharedRoot: Y.XmlText,\n key: string,\n position: Y.RelativePosition,\n) {\n sharedRoot.setAttribute(STORED_POSITION_PREFIX + key, Y.encodeRelativePosition(position))\n}\n\nfunction getAbsolutePositionsInTextRange(\n absolutePositions: Record<string, Y.AbsolutePosition>,\n yTarget: Y.XmlText,\n textRange?: TextRange,\n) {\n return Object.fromEntries(\n Object.entries(absolutePositions).filter(([, position]) => {\n if (position.type !== yTarget) {\n return false\n }\n\n if (!textRange) {\n return true\n }\n\n return position.assoc >= 0\n ? position.index >= textRange.start && position.index < textRange.end\n : position.index > textRange.start && position.index >= textRange.end\n }),\n )\n}\n\nfunction getAbsolutePositionsInYText(\n absolutePositions: Record<string, Y.AbsolutePosition>,\n yText: Y.XmlText,\n parentPath = '',\n): Record<string, Record<string, Y.AbsolutePosition>> {\n const positions = {\n [parentPath]: getAbsolutePositionsInTextRange(absolutePositions, yText),\n }\n\n const insertDelta = yTextToInsertDelta(yText)\n\n insertDelta.forEach(({ insert }, i) => {\n if (insert instanceof Y.XmlText) {\n Object.assign(\n positions,\n getAbsolutePositionsInYText(\n absolutePositions,\n insert,\n parentPath ? `${parentPath}.${i}` : i.toString(),\n ),\n )\n }\n })\n\n return positions\n}\n\nexport function getStoredPositionsInDeltaAbsolute(\n sharedRoot: Y.XmlText,\n yText: Y.XmlText,\n delta: InsertDelta,\n deltaOffset = 0,\n) {\n const absolutePositions = getStoredPositionsAbsolute(sharedRoot)\n\n const positions = {\n '': getAbsolutePositionsInTextRange(absolutePositions, yText, {\n start: deltaOffset,\n end: deltaOffset + getInsertDeltaLength(delta),\n }),\n }\n\n delta.forEach(({ insert }, i) => {\n if (insert instanceof Y.XmlText) {\n Object.assign(positions, getAbsolutePositionsInYText(absolutePositions, insert, i.toString()))\n }\n })\n\n return positions\n}\n\nexport function restoreStoredPositionsWithDeltaAbsolute(\n sharedRoot: Y.XmlText,\n yText: Y.XmlText,\n absolutePositions: Record<string, Record<string, Y.AbsolutePosition>>,\n delta: InsertDelta,\n newDeltaOffset = 0,\n previousDeltaOffset = 0,\n path = '',\n) {\n const toRestore = absolutePositions[path]\n\n if (toRestore) {\n Object.entries(toRestore).forEach(([key, position]) => {\n setStoredPosition(\n sharedRoot,\n key,\n Y.createRelativePositionFromTypeIndex(\n yText,\n position.index - previousDeltaOffset + newDeltaOffset,\n position.assoc,\n ),\n )\n })\n }\n\n delta.forEach(({ insert }, i) => {\n if (insert instanceof Y.XmlText) {\n restoreStoredPositionsWithDeltaAbsolute(\n sharedRoot,\n insert,\n absolutePositions,\n yTextToInsertDelta(insert),\n 0,\n 0,\n path ? `${path}.${i}` : i.toString(),\n )\n }\n })\n}\n\nexport function slateRangeToRelativeRange(\n sharedRoot: Y.XmlText,\n slateRoot: Node,\n range: BaseRange,\n): RelativeRange {\n return {\n anchor: slatePointToRelativePosition(sharedRoot, slateRoot, range.anchor),\n focus: slatePointToRelativePosition(sharedRoot, slateRoot, range.focus),\n }\n}\n\nexport function relativeRangeToSlateRange(\n sharedRoot: Y.XmlText,\n slateRoot: Node,\n range: RelativeRange,\n): BaseRange | null {\n const anchor = relativePositionToSlatePoint(sharedRoot, slateRoot, range.anchor)\n\n if (!anchor) {\n return null\n }\n\n const focus = relativePositionToSlatePoint(sharedRoot, slateRoot, range.focus)\n\n if (!focus) {\n return null\n }\n\n return { anchor, focus }\n}\n","import { BaseText, Descendant, Text } from 'slate'\n\nimport { omit } from './object'\n\nexport function getProperties<TNode extends Descendant>(\n node: TNode,\n): Omit<TNode, TNode extends BaseText ? 'text' : 'children'> {\n return omit(node, (Text.isText(node) ? 'text' : 'children') as keyof TNode) as Omit<\n TNode,\n TNode extends BaseText ? 'text' : 'children'\n >\n}\n","import { BaseElement } from 'packages/custom-types'\nimport { Element, Node, Text } from 'slate'\nimport * as Y from 'yjs'\n\nimport { DeltaInsert, InsertDelta } from '../module/custom-types'\nimport { yTextToInsertDelta } from './delta'\nimport { getProperties } from './slate'\n\nexport function yTextToSlateElement(yText: Y.XmlText): Element {\n const delta = yTextToInsertDelta(yText)\n\n // eslint-disable-next-line @typescript-eslint/no-use-before-define\n const children = delta.length > 0 ? delta.map(deltaInsertToSlateNode) : [{ text: '' }]\n\n // @ts-ignore\n return { ...yText.getAttributes(), children }\n}\n\nexport function deltaInsertToSlateNode(insert: DeltaInsert): Node {\n if (typeof insert.insert === 'string') {\n return { ...insert.attributes, text: insert.insert }\n }\n\n return yTextToSlateElement(insert.insert)\n}\n\nexport function slateNodesToInsertDelta(nodes: Node[]): InsertDelta {\n return nodes.map(node => {\n if (Text.isText(node)) {\n return { insert: node.text, attributes: getProperties(node) }\n }\n\n // eslint-disable-next-line @typescript-eslint/no-use-before-define\n return { insert: slateElementToYText(node as BaseElement) }\n })\n}\n\nexport function slateElementToYText({ children, ...attributes }: Element): Y.XmlText {\n const yElement = new Y.XmlText()\n\n Object.entries(attributes).forEach(([key, value]) => {\n yElement.setAttribute(key, value)\n })\n\n yElement.applyDelta(slateNodesToInsertDelta(children), { sanitize: false })\n return yElement\n}\n","import {\n Editor, Element, Node, Operation, Path, Text,\n} from 'slate'\nimport * as Y from 'yjs'\n\nimport { Delta } from '../module/custom-types'\nimport { deltaInsertToSlateNode } from '../utils/convert'\nimport { getSlateNodeYLength, getSlatePath, yOffsetToSlateOffsets } from '../utils/location'\nimport { deepEquals, omitNullEntries, pick } from '../utils/object'\nimport { getProperties } from '../utils/slate'\n\nfunction applyDelta(node: Element, slatePath: Path, delta: Delta): Operation[] {\n const ops: Operation[] = []\n\n let yOffset = delta.reduce((length, change) => {\n if ('retain' in change) {\n return length + change.retain\n }\n\n if ('delete' in change) {\n return length + change.delete\n }\n\n return length\n }, 0)\n\n // Apply changes in reverse order to avoid path changes.\n delta.reverse().forEach(change => {\n if ('attributes' in change && 'retain' in change) {\n const [startPathOffset, startTextOffset] = yOffsetToSlateOffsets(\n node,\n yOffset - change.retain,\n )\n const [endPathOffset, endTextOffset] = yOffsetToSlateOffsets(node, yOffset, { assoc: -1 })\n\n for (let pathOffset = endPathOffset; pathOffset >= startPathOffset; pathOffset -= 1) {\n const child = node.children[pathOffset]\n const childPath = [...slatePath, pathOffset]\n\n if (!Text.isText(child)) {\n // Ignore attribute updates on non-text nodes (which are backed by Y.XmlText)\n // to be consistent with deltaInsertToSlateNode. Y.XmlText attributes don't show\n // up in deltas but in key changes (YEvent.changes.keys).\n continue\n }\n\n const newProperties = change.attributes\n const properties = pick(node, ...(Object.keys(change.attributes) as Array<keyof Element>))\n\n if (pathOffset === startPathOffset || pathOffset === endPathOffset) {\n const start = pathOffset === startPathOffset ? startTextOffset : 0\n const end = pathOffset === endPathOffset ? endTextOffset : child.text.length\n\n if (end !== child.text.length) {\n ops.push({\n type: 'split_node',\n path: childPath,\n position: end,\n properties: getProperties(child),\n })\n }\n\n if (start !== 0) {\n ops.push({\n type: 'split_node',\n path: childPath,\n position: start,\n properties: omitNullEntries({\n ...getProperties(child),\n ...newProperties,\n }),\n })\n\n continue\n }\n }\n\n ops.push({\n type: 'set_node',\n newProperties,\n path: childPath,\n properties,\n })\n }\n }\n\n if ('retain' in change) {\n yOffset -= change.retain\n }\n\n if ('delete' in change) {\n const [startPathOffset, startTextOffset] = yOffsetToSlateOffsets(\n node,\n yOffset - change.delete,\n )\n const [endPathOffset, endTextOffset] = yOffsetToSlateOffsets(node, yOffset, { assoc: -1 })\n\n for (\n let pathOffset = endTextOffset === 0 ? endPathOffset - 1 : endPathOffset;\n pathOffset >= startPathOffset;\n pathOffset -= 1\n ) {\n const child = node.children[pathOffset]\n const childPath = [...slatePath, pathOffset]\n\n if (\n Text.isText(child)\n && (pathOffset === startPathOffset || pathOffset === endPathOffset)\n ) {\n const start = pathOffset === startPathOffset ? startTextOffset : 0\n const end = pathOffset === endPathOffset ? endTextOffset : child.text.length\n\n ops.push({\n type: 'remove_text',\n offset: start,\n text: child.text.slice(start, end),\n path: childPath,\n })\n\n yOffset -= end - start\n continue\n }\n\n ops.push({\n type: 'remove_node',\n node: child,\n path: childPath,\n })\n yOffset -= getSlateNodeYLength(child)\n }\n\n return\n }\n\n if ('insert' in change) {\n const [pathOffset, textOffset] = yOffsetToSlateOffsets(node, yOffset, {\n insert: true,\n })\n const child = node.children[pathOffset]\n const childPath = [...slatePath, pathOffset]\n\n if (Text.isText(child)) {\n const lastOp = ops[ops.length - 1]\n\n /**\n * The props that exist at the current path\n * Since we're not actually using slate to update the node\n * this is a simulation\n */\n const currentProps = lastOp != null && lastOp.type === 'insert_node' ? lastOp.node : getProperties(child)\n\n let lastPath: Path = []\n\n if (\n lastOp != null\n && (lastOp.type === 'insert_node'\n || lastOp.type === 'insert_text'\n || lastOp.type === 'split_node'\n || lastOp.type === 'set_node')\n ) {\n lastPath = lastOp.path\n }\n\n /**\n * If the insert is a string and the attributes are the same as the\n * props at the current path, we can just insert a text node\n */\n if (\n typeof change.insert === 'string'\n && deepEquals(change.attributes ?? {}, currentProps)\n && Path.equals(childPath, lastPath)\n ) {\n return ops.push({\n type: 'insert_text',\n offset: textOffset,\n text: change.insert,\n path: childPath,\n })\n }\n\n const toInsert = deltaInsertToSlateNode(change)\n\n if (textOffset === 0) {\n return ops.push({\n type: 'insert_node',\n path: childPath,\n node: toInsert,\n })\n }\n\n if (textOffset < child.text.length) {\n ops.push({\n type: 'split_node',\n path: childPath,\n position: textOffset,\n properties: getProperties(child),\n })\n }\n\n return ops.push({\n type: 'insert_node',\n path: Path.next(childPath),\n node: toInsert,\n })\n }\n\n return ops.push({\n type: 'insert_node',\n path: childPath,\n node: deltaInsertToSlateNode(change),\n })\n }\n })\n\n return ops\n}\n\nexport function translateYTextEvent(\n sharedRoot: Y.XmlText,\n editor: Editor,\n event: Y.YTextEvent,\n): Operation[] {\n const { target, changes } = event\n const delta = event.delta as Delta\n\n if (!(target instanceof Y.XmlText)) {\n throw new Error('Unexpected target node type')\n }\n\n const ops: Operation[] = []\n const slatePath = getSlatePath(sharedRoot, editor, target)\n const targetElement = Node.get(editor, slatePath)\n\n if (Text.isText(targetElement)) {\n throw new Error('Cannot apply yTextEvent to text node')\n }\n\n const keyChanges = Array.from(changes.keys.entries())\n\n if (slatePath.length > 0 && keyChanges.length > 0) {\n const newProperties = Object.fromEntries(\n keyChanges.map(([key, info]) => [\n key,\n info.action === 'delete' ? null : target.getAttribute(key),\n ]),\n )\n\n const properties = Object.fromEntries(keyChanges.map(([key]) => [key, targetElement[key]]))\n\n ops.push({\n type: 'set_node', newProperties, properties, path: slatePath,\n })\n }\n\n if (delta.length > 0) {\n ops.push(...applyDelta(targetElement as Element, slatePath, delta))\n }\n\n return ops\n}\n","import { Editor, Operation } from 'slate'\nimport * as Y from 'yjs'\n\nimport { translateYTextEvent } from './textEvent'\n\n/**\n * Translate a yjs event into slate operations. The editor state has to match the\n * yText state before the event occurred.\n *\n * @param sharedType\n * @param op\n */\nexport function translateYjsEvent(\n sharedRoot: Y.XmlText,\n editor: Editor,\n event: Y.YEvent<Y.XmlText>,\n): Operation[] {\n if (event instanceof Y.YTextEvent) {\n return translateYTextEvent(sharedRoot, editor, event)\n }\n\n throw new Error('Unexpected Y event type')\n}\n\n/**\n * Translates yjs events into slate operations and applies them to the editor. The\n * editor state has to match the yText state before the events occurred.\n *\n * @param sharedRoot\n * @param editor\n * @param events\n */\nexport function applyYjsEvents(\n sharedRoot: Y.XmlText,\n editor: Editor,\n events: Y.YEvent<Y.XmlText>[],\n) {\n Editor.withoutNormalizing(editor, () => {\n events.forEach(event => {\n translateYjsEvent(sharedRoot, editor, event).forEach(op => {\n editor.apply(op)\n })\n })\n })\n}\n","import * as Y from 'yjs'\n\nimport { InsertDelta } from '../module/custom-types'\nimport { yTextToInsertDelta } from './delta'\n\nexport function cloneInsertDeltaDeep(delta: InsertDelta): InsertDelta {\n return delta.map(element => {\n if (typeof element.insert === 'string') {\n return element\n }\n\n // eslint-disable-next-line @typescript-eslint/no-use-before-define\n return { ...element, insert: cloneDeep(element.insert) }\n })\n}\n\nexport function cloneDeep(yText: Y.XmlText): Y.XmlText {\n const clone = new Y.XmlText()\n\n const attributes = yText.getAttributes()\n\n Object.entries(attributes).forEach(([key, value]) => {\n clone.setAttribute(key, value)\n })\n\n clone.applyDelta(cloneInsertDeltaDeep(yTextToInsertDelta(yText)), {\n sanitize: false,\n })\n\n return clone\n}\n","import { NodeOperation } from 'slate'\n\nimport { OpMapper } from '../types'\nimport { insertNode } from './insertNode'\nimport { mergeNode } from './mergeNode'\nimport { moveNode } from './moveNode'\nimport { removeNode } from './removeNode'\nimport { setNode } from './setNode'\nimport { splitNode } from './splitNode'\n\nexport const NODE_MAPPER: OpMapper<NodeOperation> = {\n insert_node: insertNode,\n remove_node: removeNode,\n set_node: setNode,\n merge_node: mergeNode,\n move_node: moveNode,\n split_node: splitNode,\n}\n","import { CustomElement } from 'packages/custom-types'\nimport { InsertNodeOperation, Node, Text } from 'slate'\nimport * as Y from 'yjs'\n\nimport { slateElementToYText } from '../../utils/convert'\nimport { getYTarget } from '../../utils/location'\nimport { getProperties } from '../../utils/slate'\n\nexport function insertNode(sharedRoot: Y.XmlText, slateRoot: Node, op: InsertNodeOperation): void {\n const { yParent, textRange } = getYTarget(sharedRoot, slateRoot, op.path)\n\n if (Text.isText(op.node)) {\n return yParent.insert(textRange.start, op.node.text, getProperties(op.node))\n }\n\n yParent.insertEmbed(textRange.start, slateElementToYText(op.node as CustomElement))\n}\n","import { Node, RemoveNodeOperation } from 'slate'\nimport * as Y from 'yjs'\n\nimport { getYTarget } from '../../utils/location'\n\nexport function removeNode(sharedRoot: Y.XmlText, slateRoot: Node, op: RemoveNodeOperation): void {\n const { yParent: parent, textRange } = getYTarget(sharedRoot, slateRoot, op.path)\n\n parent.delete(textRange.start, textRange.end - textRange.start)\n}\n","import { Node, SetNodeOperation } from 'slate'\nimport * as Y from 'yjs'\n\nimport { getYTarget } from '../../utils/location'\n\nexport function setNode(sharedRoot: Y.XmlText, slateRoot: Node, op: SetNodeOperation): void {\n const { yTarget, textRange, yParent } = getYTarget(sharedRoot, slateRoot, op.path)\n\n if (yTarget) {\n Object.entries(op.newProperties).forEach(([key, value]) => {\n if (value === null) {\n return yTarget.removeAttribute(key)\n }\n\n yTarget.setAttribute(key, value)\n })\n\n return Object.entries(op.properties).forEach(([key]) => {\n if (!Object.prototype.hasOwnProperty.call(op.newProperties, key)) {\n yTarget.removeAttribute(key)\n }\n })\n }\n\n const unset = Object.fromEntries(Object.keys(op.properties).map(key => [key, null]))\n const newProperties = { ...unset, ...op.newProperties }\n\n yParent.format(textRange.start, textRange.end - textRange.start, newProperties)\n}\n","import { BaseElement } from 'packages/custom-types'\nimport {\n MergeNodeOperation, Node, Path, Text,\n} from 'slate'\nimport * as Y from 'yjs'\n\nimport { Delta } from '../../module/custom-types'\nimport { cloneInsertDeltaDeep } from '../../utils/clone'\nimport { yTextToInsertDelta } from '../../utils/delta'\nimport { getYTarget } from '../../utils/location'\nimport {\n getStoredPositionsInDeltaAbsolute,\n restoreStoredPositionsWithDeltaAbsolute,\n} from '../../utils/position'\nimport { getProperties } from '../../utils/slate'\n\nexport function mergeNode(sharedRoot: Y.XmlText, slateRoot: Node, op: MergeNodeOperation): void {\n const target = getYTarget(sharedRoot, slateRoot, op.path)\n const prev = getYTarget(target.yParent, target.slateParent, Path.previous(op.path.slice(-1)))\n\n if (!target.yTarget !== !prev.yTarget) {\n throw new Error('Cannot merge y text with y element')\n }\n\n if (!prev.yTarget || !target.yTarget) {\n const { yParent: parent, textRange, slateTarget } = target\n\n if (!slateTarget) {\n throw new Error('Expected Slate target node for merge op.')\n }\n\n const prevSibling = Node.get(slateRoot, Path.previous(op.path))\n\n if (!Text.isText(prevSibling)) {\n throw new Error('Path points to Y.Text but not a Slate text node.')\n }\n\n const targetProps = getProperties(slateTarget as BaseElement)\n const prevSiblingProps = getProperties(prevSibling)\n const unsetProps = Object.keys(targetProps).reduce((acc, key) => {\n const prevSiblingHasProp = key in prevSiblingProps\n\n return prevSiblingHasProp ? acc : { ...acc, [key]: null }\n }, {})\n\n return parent.format(textRange.start, textRange.end - textRange.start, {\n ...unsetProps,\n ...prevSiblingProps,\n })\n }\n\n const deltaApplyYOffset = prev.yTarget.length\n const targetDelta = yTextToInsertDelta(target.yTarget)\n const clonedDelta = cloneInsertDeltaDeep(targetDelta)\n\n const storedPositions = getStoredPositionsInDeltaAbsolute(\n sharedRoot,\n target.yTarget,\n targetDelta,\n deltaApplyYOffset,\n )\n\n const applyDelta: Delta = [{ retain: deltaApplyYOffset }, ...clonedDelta]\n\n prev.yTarget.applyDelta(applyDelta, {\n sanitize: false,\n })\n\n target.yParent.delete(target.textRange.start, target.textRange.end - target.textRange.start)\n\n restoreStoredPositionsWithDeltaAbsolute(\n sharedRoot,\n prev.yTarget,\n storedPositions,\n clonedDelta,\n deltaApplyYOffset,\n )\n}\n","import {\n MoveNodeOperation, Node, Path, Text,\n} from 'slate'\nimport * as Y from 'yjs'\n\nimport { Delta } from '../../module/custom-types'\nimport { cloneInsertDeltaDeep } from '../../utils/clone'\nimport { getInsertDeltaLength, yTextToInsertDelta } from '../../utils/delta'\nimport { getYTarget } from '../../utils/location'\nimport {\n getStoredPositionsInDeltaAbsolute,\n restoreStoredPositionsWithDeltaAbsolute,\n} from '../../utils/position'\n\nexport function moveNode(sharedRoot: Y.XmlText, slateRoot: Node, op: MoveNodeOperation): void {\n const newParentPath = Path.parent(op.newPath)\n const newPathOffset = op.newPath[op.newPath.length - 1]\n const parent = Node.get(slateRoot, newParentPath)\n\n if (Text.isText(parent)) {\n throw new Error('Cannot move slate node into text element')\n }\n const normalizedNewPath = [...newParentPath, Math.min(newPathOffset, parent.children.length)]\n\n const origin = getYTarget(sharedRoot, slateRoot, op.path)\n const target = getYTarget(sharedRoot, slateRoot, normalizedNewPath)\n const insertDelta = cloneInsertDeltaDeep(origin.targetDelta)\n\n const storedPositions = getStoredPositionsInDeltaAbsolute(\n sharedRoot,\n origin.yParent,\n origin.targetDelta,\n )\n\n origin.yParent.delete(origin.textRange.start, origin.textRange.end - origin.textRange.start)\n\n const targetLength = getInsertDeltaLength(yTextToInsertDelta(target.yParent))\n const deltaApplyYOffset = Math.min(target.textRange.start, targetLength)\n const applyDelta: Delta = [{ retain: deltaApplyYOffset }, ...insertDelta]\n\n target.yParent.applyDelta(applyDelta, { sanitize: false })\n\n restoreStoredPositionsWithDeltaAbsolute(\n sharedRoot,\n target.yParent,\n storedPositions,\n insertDelta,\n deltaApplyYOffset,\n origin.textRange.start,\n )\n}\n","import { Node, SplitNodeOperation, Text } from 'slate'\nimport * as Y from 'yjs'\n\nimport { cloneInsertDeltaDeep } from '../../utils/clone'\nimport { sliceInsertDelta, yTextToInsertDelta } from '../../utils/delta'\nimport { getSlateNodeYLength, getYTarget } from '../../utils/location'\nimport {\n getStoredPositionsInDeltaAbsolute,\n restoreStoredPositionsWithDeltaAbsolute,\n} from '../../utils/position'\n\nexport function splitNode(sharedRoot: Y.XmlText, slateRoot: Node, op: SplitNodeOperation): void {\n const target = getYTarget(sharedRoot, slateRoot, op.path)\n\n if (!target.slateTarget) {\n throw new Error('Y target without corresponding slate node')\n }\n\n if (!target.yTarget) {\n if (!Text.isText(target.slateTarget)) {\n throw new Error('Mismatch node type between y target and slate node')\n }\n\n const unset: Record<string, null> = {}\n\n target.targetDelta.forEach(element => {\n if (element.attributes) {\n Object.keys(element.attributes).forEach(key => {\n unset[key] = null\n })\n }\n })\n\n return target.yParent.format(\n target.textRange.start,\n target.textRange.end - target.textRange.start,\n { ...unset, ...op.properties },\n )\n }\n\n if (Text.isText(target.slateTarget)) {\n throw new Error('Mismatch node type between y target and slate node')\n }\n\n const splitTarget = getYTarget(target.yTarget, target.slateTarget, [op.position])\n\n const ySplitOffset = target.slateTarget.children\n .slice(0, op.position)\n .reduce((length, child) => length + getSlateNodeYLength(child), 0)\n\n // @ts-ignore\n const length = target.slateTarget.children.reduce(\n (current, child) => current + getSlateNodeYLength(child),\n 0,\n )\n\n const splitDelta = sliceInsertDelta(\n yTextToInsertDelta(target.yTarget),\n ySplitOffset,\n length - ySplitOffset,\n )\n const clonedDelta = cloneInsertDeltaDeep(splitDelta)\n\n const storedPositions = getStoredPositionsInDeltaAbsolute(\n sharedRoot,\n target.yTarget,\n splitDelta,\n ySplitOffset,\n )\n\n const toInsert = new Y.XmlText()\n\n toInsert.applyDelta(clonedDelta, {\n sanitize: false,\n })\n\n Object.entries(op.properties).forEach(([key, value]) => {\n toInsert.setAttribute(key, value)\n })\n\n target.yTarget.delete(\n splitTarget.textRange.start,\n target.yTarget.length - splitTarget.textRange.start,\n )\n\n target.yParent.insertEmbed(target.textRange.end, toInsert)\n\n restoreStoredPositionsWithDeltaAbsolute(\n sharedRoot,\n toInsert,\n storedPositions,\n clonedDelta,\n 0,\n ySplitOffset,\n )\n}\n","import { TextOperation } from 'slate'\n\nimport { OpMapper } from '../types'\nimport { insertText } from './insertText'\nimport { removeText } from './removeText'\n\nexport const TEXT_MAPPER: OpMapper<TextOperation> = {\n insert_text: insertText,\n remove_text: removeText,\n}\n","import { Node, Operation } from 'slate'\nimport * as Y from 'yjs'\n\nimport { NODE_MAPPER } from './node'\nimport { TEXT_MAPPER } from './text'\nimport { ApplyFunc, OpMapper } from './types'\n\n// eslint-disable-next-line @typescript-eslint/no-empty-function\nconst NOOP = () => {}\n\nconst opMappers: OpMapper = {\n ...TEXT_MAPPER,\n ...NODE_MAPPER,\n\n set_selection: NOOP,\n}\n\nexport function applySlateOp(sharedRoot: Y.XmlText, slateRoot: Node, op: Operation): void {\n const apply = opMappers[op.type] as ApplyFunc<typeof op>\n\n if (!apply) {\n throw new Error(`Unknown operation: ${op.type}`)\n }\n\n apply(sharedRoot, slateRoot, op)\n}\n","import { InsertTextOperation, Node, Text } from 'slate'\nimport * as Y from 'yjs'\n\nimport { getYTarget } from '../../utils/location'\nimport { getProperties } from '../../utils/slate'\n\nexport function insertText(sharedRoot: Y.XmlText, slateRoot: Node, op: InsertTextOperation): void {\n const { yParent: target, textRange } = getYTarget(sharedRoot, slateRoot, op.path)\n\n const targetNode = Node.get(slateRoot, op.path)\n\n if (!Text.isText(targetNode)) {\n throw new Error('Cannot insert text into non-text node')\n }\n\n target.insert(textRange.start + op.offset, op.text, getProperties(targetNode))\n}\n","import { Node, RemoveTextOperation } from 'slate'\nimport * as Y from 'yjs'\n\nimport { getYTarget } from '../../utils/location'\n\nexport function removeText(sharedRoot: Y.XmlText, slateRoot: Node, op: RemoveTextOperation): void {\n const { yParent: target, textRange } = getYTarget(sharedRoot, slateRoot, op.path)\n\n target.delete(textRange.start + op.offset, op.text.length)\n}\n","import {\n BaseEditor, Descendant, Editor, Operation, Point,\n} from 'slate'\nimport * as Y from 'yjs'\n\nimport { applyYjsEvents } from '../applyToSlate'\nimport { applySlateOp } from '../applyToYjs'\nimport { yTextToSlateElement } from '../utils/convert'\nimport {\n getStoredPosition,\n getStoredPositions,\n relativePositionToSlatePoint,\n removeStoredPosition,\n setStoredPosition,\n slatePointToRelativePosition,\n} from '../utils/position'\nimport { assertDocumentAttachment } from '../utils/yjs'\n\ntype LocalChange = {\n op: Operation\n doc: Descendant[]\n origin: unknown\n}\n\nconst DEFAULT_LOCAL_ORIGIN = Symbol('slate-yjs-operation')\nconst DEFAULT_POSITION_STORAGE_ORIGIN = Symbol('slate-yjs-position-storage')\n\nconst ORIGIN: WeakMap<Editor, unknown> = new WeakMap()\nconst LOCAL_CHANGES: WeakMap<Editor, LocalChange[]> = new WeakMap()\nconst CONNECTED: WeakSet<Editor> = new WeakSet()\n\nexport type YjsEditor = BaseEditor & {\n sharedRoot: Y.XmlText\n\n localOrigin: unknown\n positionStorageOrigin: unknown\n\n applyRemoteEvents: (events: Y.YEvent<Y.XmlText>[], origin: unknown) => void\n\n storeLocalChange: (op: Operation) => void\n flushLocalChanges: () => void\n\n isLocalOrigin: (origin: unknown) => boolean\n\n connect: () => void\n disconnect: () => void\n}\n\n// eslint-disable-next-line @typescript-eslint/no-redeclare\nexport const YjsEditor = {\n isYjsEditor(value: unknown): value is YjsEditor {\n return (\n Editor.isEditor(value)\n && (value as YjsEditor).sharedRoot instanceof Y.XmlText\n && 'localOrigin' in value\n && 'positionStorageOrigin' in value\n && typeof (value as YjsEditor).applyRemoteEvents === 'function'\n && typeof (value as YjsEditor).storeLocalChange === 'function'\n && typeof (value as YjsEditor).flushLocalChanges === 'function'\n && typeof (value as YjsEditor).isLocalOrigin === 'function'\n && typeof (value as YjsEditor).connect === 'function'\n && typeof (value as YjsEditor).disconnect === 'function'\n )\n },\n\n localChanges(editor: YjsEditor): LocalChange[] {\n return LOCAL_CHANGES.get(editor) ?? []\n },\n\n applyRemoteEvents(editor: YjsEditor, events: Y.YEvent<Y.XmlText>[], origin: unknown): void {\n editor.applyRemoteEvents(events, origin)\n },\n\n storeLocalChange(editor: YjsEditor, op: Operation): void {\n editor.storeLocalChange(op)\n },\n\n flushLocalChanges(editor: YjsEditor): void {\n editor.flushLocalChanges()\n },\n\n connected(editor: YjsEditor): boolean {\n return CONNECTED.has(editor)\n },\n\n connect(editor: YjsEditor): void {\n editor.connect()\n },\n\n disconnect(editor: YjsEditor): void {\n editor.disconnect()\n },\n\n isLocal(editor: YjsEditor): boolean {\n return editor.isLocalOrigin(YjsEditor.origin(editor))\n },\n\n origin(editor: YjsEditor): unknown {\n const origin = ORIGIN.get(editor)\n\n return origin !== undefined ? origin : editor.localOrigin\n },\n\n withOrigin(editor: YjsEditor, origin: unknown, fn: () => void): void {\n const prev = YjsEditor.origin(editor)\n\n ORIGIN.set(editor, origin)\n fn()\n ORIGIN.set(editor, prev)\n },\n\n storePosition(editor: YjsEditor, key: string, point: Point): void {\n const { sharedRoot, positionStorageOrigin: locationStorageOrigin } = editor\n\n assertDocumentAttachment(sharedRoot)\n\n const position = slatePointToRelativePosition(sharedRoot, editor, point)\n\n sharedRoot.doc.transact(() => {\n setStoredPosition(sharedRoot, key, position)\n }, locationStorageOrigin)\n },\n\n removeStoredPosition(editor: YjsEditor, key: string): void {\n const { sharedRoot, positionStorageOrigin: locationStorageOrigin } = editor\n\n assertDocumentAttachment(sharedRoot)\n\n sharedRoot.doc.transact(() => {\n removeStoredPosition(sharedRoot, key)\n }, locationStorageOrigin)\n },\n\n position(editor: YjsEditor, key: string): Point | null | undefined {\n const position = getStoredPosition(editor.sharedRoot, key)\n\n if (!position) {\n return undefined\n }\n\n return relativePositionToSlatePoint(editor.sharedRoot, editor, position)\n },\n\n storedPositionsRelative(editor: YjsEditor): Record<string, Y.RelativePosition> {\n return getStoredPositions(editor.sharedRoot)\n },\n}\n\nexport type WithYjsOptions = {\n autoConnect?: boolean\n\n // Origin used when applying local slate operations to yjs\n localOrigin?: unknown\n\n // Origin used when storing positions\n positionStorageOrigin?: unknown\n}\n\nexport function withYjs(sharedRoot: Y.XmlText, options: WithYjsOptions = {}) {\n return function <T extends Editor> (editor: T): T & YjsEditor {\n const e = editor as T & YjsEditor\n\n e.sharedRoot = sharedRoot\n\n e.localOrigin = options.localOrigin ?? DEFAULT_LOCAL_ORIGIN\n e.positionStorageOrigin = options.positionStorageOrigin ?? DEFAULT_POSITION_STORAGE_ORIGIN\n\n e.applyRemoteEvents = (events, origin) => {\n YjsEditor.flushLocalChanges(e)\n\n Editor.withoutNormalizing(e, () => {\n YjsEditor.withOrigin(e, origin, () => {\n applyYjsEvents(e.sharedRoot, e, events)\n })\n })\n }\n\n e.isLocalOrigin = origin => origin === e.localOrigin\n\n const handleYEvents = (events: Y.YEvent<Y.XmlText>[], transaction: Y.Transaction) => {\n if (e.isLocalOrigin(transaction.origin)) {\n return\n }\n\n YjsEditor.applyRemoteEvents(e, events, transaction.origin)\n }\n\n let autoConnectTimeoutId: ReturnType<typeof setTimeout> | null = null\n\n if (options.autoConnect) {\n autoConnectTimeoutId = setTimeout(() => {\n autoConnectTimeoutId = null\n YjsEditor.connect(e)\n })\n }\n\n e.connect = () => {\n if (YjsEditor.connected(e)) {\n throw new Error('already connected')\n }\n\n e.sharedRoot.observeDeep(handleYEvents)\n const content = yTextToSlateElement(e.sharedRoot)\n\n e.children = content.children\n CONNECTED.add(e)\n\n Editor.normalize(editor, { force: true })\n if (!editor.operations.length) {\n editor.onChange()\n }\n }\n\n e.disconnect = () => {\n if (autoConnectTimeoutId) {\n clearTimeout(autoConnectTimeoutId)\n }\n\n YjsEditor.flushLocalChanges(e)\n e.sharedRoot.unobserveDeep(handleYEvents)\n CONNECTED.delete(e)\n }\n\n e.storeLocalChange = op => {\n LOCAL_CHANGES.set(e, [\n ...YjsEditor.localChanges(e),\n { op, doc: editor.children, origin: YjsEditor.origin(e) },\n ])\n }\n\n e.flushLocalChanges = () => {\n assertDocumentAttachment(e.sharedRoot)\n const localChanges = YjsEditor.localChanges(e)\n\n LOCAL_CHANGES.delete(e)\n\n const txGroups: LocalChange[][] = []\n\n localChanges.forEach(change => {\n const currentGroup = txGroups[txGroups.length - 1]\n\n if (currentGroup && currentGroup[0].origin === change.origin) {\n return currentGroup.push(change)\n }\n\n txGroups.push([change])\n })\n\n txGroups.forEach(txGroup => {\n assertDocumentAttachment(e.sharedRoot)\n\n e.sharedRoot.doc.transact(() => {\n txGroup.forEach(change => {\n assertDocumentAttachment(e.sharedRoot)\n // @ts-ignore\n applySlateOp(e.sharedRoot, { children: change.doc }, change.op)\n })\n }, txGroup[0].origin)\n })\n }\n\n const { apply, onChange } = e\n\n e.apply = op => {\n if (YjsEditor.connected(e) && YjsEditor.isLocal(e)) {\n YjsEditor.storeLocalChange(e, op)\n }\n\n apply(op)\n }\n\n e.onChange = () => {\n if (YjsEditor.connected(e)) {\n YjsEditor.flushLocalChanges(e)\n }\n\n onChange()\n }\n\n return e\n }\n}\n","import { Editor, Range } from 'slate'\nimport { Awareness } from 'y-protocols/awareness'\nimport * as Y from 'yjs'\n\nimport { RelativeRange } from '../module/custom-types'\nimport { slateRangeToRelativeRange } from '../utils/position'\nimport { YjsEditor } from './withYjs'\n\nexport type CursorStateChangeEvent = {\n added: number[]\n updated: number[]\n removed: number[]\n}\n\nexport type RemoteCursorChangeEventListener = (event: CursorStateChangeEvent) => void\n\nconst CURSOR_CHANGE_EVENT_LISTENERS: WeakMap<\n Editor,\n Set<RemoteCursorChangeEventListener>\n> = new WeakMap()\n\nexport type CursorState<TCursorData extends Record<string, unknown> = Record<string, unknown>> = {\n relativeSelection: RelativeRange | null\n data?: TCursorData\n clientId: number\n}\n\nexport type CursorEditor<TCursorData extends Record<string, unknown> = Record<string, unknown>> =\n YjsEditor & {\n awareness: Awareness\n\n cursorDataField: string\n selectionStateField: string\n\n sendCursorPosition: (range: Range | null) => void\n sendCursorData: (data: TCursorData) => void\n }\n\n// eslint-disable-next-line @typescript-eslint/no-redeclare\nexport const CursorEditor = {\n isCursorEditor(value: unk