diff --git a/README.md b/README.md index cedb201..7f3ea71 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ Check [detailed example](https://github.com/cudr/slate-collaborative/blob/master onConnect?: () => void // connect callback onDisconnect?: () => void // disconnect callback onError?: (reason: string) => void // error callback + preserveExternalHistory?: boolean // preserve slate-history operations form other clients } ``` diff --git a/packages/bridge/src/apply/text.ts b/packages/bridge/src/apply/text.ts index 414302d..004da2c 100644 --- a/packages/bridge/src/apply/text.ts +++ b/packages/bridge/src/apply/text.ts @@ -9,7 +9,9 @@ export const insertText = ( ): SyncValue => { const node = getTarget(doc, op.path) - node.text.insertAt(op.offset, ...op.text.split('')) + const offset = Math.min(node.text.length, op.offset) + + node.text.insertAt(offset, ...op.text.split('')) return doc } @@ -20,7 +22,9 @@ export const removeText = ( ): SyncValue => { const node = getTarget(doc, op.path) - node.text.deleteAt(op.offset, op.text.length) + const offset = Math.min(node.text.length, op.offset) + + node.text.deleteAt(offset, op.text.length) return doc } diff --git a/packages/bridge/src/convert/convert.spec.ts b/packages/bridge/src/convert/convert.spec.ts index 5d5d5ed..4278d96 100644 --- a/packages/bridge/src/convert/convert.spec.ts +++ b/packages/bridge/src/convert/convert.spec.ts @@ -54,15 +54,13 @@ describe('convert operations to slatejs model', () => { { type: 'remove_node', path: [1], - node: { - text: '*' - } + node: createNode('paragraph', 'hello twice!') }, { type: 'remove_node', path: [0, 0], node: { - text: '*' + children: [] } } ] diff --git a/packages/bridge/src/convert/create.ts b/packages/bridge/src/convert/create.ts index 2cb8135..9cf44c8 100644 --- a/packages/bridge/src/convert/create.ts +++ b/packages/bridge/src/convert/create.ts @@ -1,6 +1,6 @@ import * as Automerge from 'automerge' -const createByType = (type: any) => +const createByType = (type: Automerge.CollectionType) => type === 'map' ? {} : type === 'list' ? [] : '' const opCreate = ({ obj, type }: Automerge.Diff, [map, ops]: any) => { diff --git a/packages/bridge/src/convert/index.ts b/packages/bridge/src/convert/index.ts index 82c54b3..61c837d 100644 --- a/packages/bridge/src/convert/index.ts +++ b/packages/bridge/src/convert/index.ts @@ -5,6 +5,8 @@ import opRemove from './remove' import opSet from './set' import opCreate from './create' +import { toJS } from '../utils' + import { SyncDoc } from '../model' const byAction = { @@ -32,7 +34,9 @@ const toSlateOp = (ops: Automerge.Diff[], doc: SyncDoc) => { [] ]) - return defer.flatMap(op => op(tempTree, doc)).filter(op => op) + const tempDoc = toJS(doc) + + return defer.flatMap(op => op(tempTree, tempDoc)).filter(op => op) } export { toSlateOp } diff --git a/packages/bridge/src/convert/remove.ts b/packages/bridge/src/convert/remove.ts index 0c988d4..b9b22ab 100644 --- a/packages/bridge/src/convert/remove.ts +++ b/packages/bridge/src/convert/remove.ts @@ -1,33 +1,56 @@ import * as Automerge from 'automerge' +import { Element } from 'slate' import { toSlatePath, toJS } from '../utils' import { getTarget } from '../path' -const removeTextOp = ({ index, path }: Automerge.Diff) => () => ({ - type: 'remove_text', - path: toSlatePath(path).slice(0, path?.length), - offset: index, - text: '*', - marks: [] -}) +const removeTextOp = (op: Automerge.Diff) => (map: any, doc: Element) => { + const { index, path, obj } = op + + const slatePath = toSlatePath(path).slice(0, path?.length) + + let node + + try { + node = getTarget(doc, slatePath) || map[obj] + } catch (e) { + console.error(e, op, doc) + } + + if (typeof index !== 'number') return + + const text = node?.text[index] || '*' + + if (node) { + node.text = node.text?.slice(0, index) + node.text?.slice(index + 1) + } + + return { + type: 'remove_text', + path: slatePath, + offset: index, + text, + marks: [] + } +} const removeNodeOp = ({ index, obj, path }: Automerge.Diff) => ( map: any, - doc: any + doc: Element ) => { const slatePath = toSlatePath(path) - if (!map.hasOwnProperty(obj)) { - const target = getTarget(doc, [...slatePath, index] as any) + const parent = getTarget(doc, slatePath) + const target = parent?.children[index as number] || { children: [] } + + if (!map.hasOwnProperty(obj)) { map[obj] = target } return { type: 'remove_node', path: slatePath.length ? slatePath.concat(index) : [index], - node: { - text: '*' - } + node: target } } diff --git a/packages/bridge/src/path/index.ts b/packages/bridge/src/path/index.ts index ef63fad..ea4f554 100644 --- a/packages/bridge/src/path/index.ts +++ b/packages/bridge/src/path/index.ts @@ -1,10 +1,10 @@ -import { Node, Path } from 'slate' +import { Element, Node, Path } from 'slate' import { SyncValue } from '../model' export const isTree = (node: Node): boolean => Boolean(node?.children) -export const getTarget = (doc: SyncValue, path: Path) => { +export const getTarget = (doc: SyncValue | Element, path: Path) => { const iterate = (current: any, idx: number) => { if (!(isTree(current) || current[idx])) { throw new TypeError( @@ -30,7 +30,7 @@ export const getParentPath = ( } export const getParent = ( - doc: SyncValue, + doc: SyncValue | Element, path: Path, level = 1 ): [any, number] => { diff --git a/packages/client/package.json b/packages/client/package.json index 0ab6857..2068052 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -29,6 +29,7 @@ "@slate-collaborative/bridge": "^0.6.7", "automerge": "0.14.0", "slate": "0.58.3", + "slate-history": "0.58.3", "socket.io-client": "^2.3.0", "typescript": "^3.8.3" }, diff --git a/packages/client/src/automerge-editor.ts b/packages/client/src/automerge-editor.ts index 14811b5..c15c671 100644 --- a/packages/client/src/automerge-editor.ts +++ b/packages/client/src/automerge-editor.ts @@ -1,6 +1,7 @@ import Automerge from 'automerge' import { Editor, Operation } from 'slate' +import { HistoryEditor } from 'slate-history' import { toJS, @@ -111,7 +112,8 @@ export const AutomergeEditor = { applyOperation: ( e: AutomergeEditor, docId: string, - data: Automerge.Message + data: Automerge.Message, + preserveExternalHistory?: boolean ) => { try { const current: any = e.docSet.getDoc(docId) @@ -126,13 +128,17 @@ export const AutomergeEditor = { e.isRemote = true Editor.withoutNormalizing(e, () => { - slateOps.forEach((o: Operation) => { - e.apply(o) - }) + if (HistoryEditor.isHistoryEditor(e) && !preserveExternalHistory) { + HistoryEditor.withoutSaving(e, () => { + slateOps.forEach((o: Operation) => e.apply(o)) + }) + } else { + slateOps.forEach((o: Operation) => e.apply(o)) + } + + e.onCursor && e.onCursor(updated.cursors) }) - e.onCursor && e.onCursor(updated.cursors) - Promise.resolve().then(_ => (e.isRemote = false)) } } catch (e) { diff --git a/packages/client/src/withAutomerge.ts b/packages/client/src/withAutomerge.ts index 0c19a7e..fe3bbaf 100644 --- a/packages/client/src/withAutomerge.ts +++ b/packages/client/src/withAutomerge.ts @@ -9,6 +9,7 @@ import { CursorData, CollabAction } from '@slate-collaborative/bridge' export interface AutomergeOptions { docId: string cursorData?: CursorData + preserveExternalHistory?: boolean } /** @@ -23,7 +24,7 @@ const withAutomerge = ( const { onChange } = e - const { docId, cursorData } = options || {} + const { docId, cursorData, preserveExternalHistory } = options || {} e.docSet = new Automerge.DocSet() @@ -73,11 +74,9 @@ const withAutomerge = ( if (!e.isRemote) { AutomergeEditor.applySlateOps(e, docId, operations, cursorData) - } - - onChange() - // console.log('e', e.children) + onChange() + } } /** @@ -97,7 +96,7 @@ const withAutomerge = ( e.receiveOperation = data => { if (docId !== data.docId) return - AutomergeEditor.applyOperation(e, docId, data) + AutomergeEditor.applyOperation(e, docId, data, preserveExternalHistory) } return e