diff --git a/packages/backend/src/Connection.ts b/packages/backend/src/Connection.ts index 6a81e63..3e88218 100644 --- a/packages/backend/src/Connection.ts +++ b/packages/backend/src/Connection.ts @@ -32,6 +32,8 @@ class Connection { private appendDoc = (path: string, value: ValueJSON) => { const sync = toSync(value) + sync.cursors = {} + const doc = Automerge.from(sync) this.docSet.setDoc(path, doc) diff --git a/packages/bridge/src/apply/annotation.ts b/packages/bridge/src/apply/annotation.ts index 083b660..8650aed 100644 --- a/packages/bridge/src/apply/annotation.ts +++ b/packages/bridge/src/apply/annotation.ts @@ -1,14 +1,17 @@ import { Operation, SyncDoc } from '../model' export const addAnnotation = (doc: SyncDoc, op: Operation): SyncDoc => { + console.log('addAnnotation!!!', op) return doc } export const removeAnnotation = (doc: SyncDoc, op: Operation): SyncDoc => { + console.log('removeAnnotation!!!', op) return doc } export const setAnnotation = (doc: SyncDoc, op: Operation): SyncDoc => { + console.log('setAnnotation!!!', op) return doc } diff --git a/packages/bridge/src/apply/index.ts b/packages/bridge/src/apply/index.ts index 7af5423..1a19ae1 100644 --- a/packages/bridge/src/apply/index.ts +++ b/packages/bridge/src/apply/index.ts @@ -5,7 +5,31 @@ import mark from './mark' import text from './text' import annotation from './annotation' -const setSelection = doc => doc +const setSelection = (doc, op, { id, selection }) => { + console.log('selection', selection.toJSON()) + if (!doc.cursors) { + doc.cursors = {} + } + + // console.log('setSelection', op.toJSON(), id) + const operation = op.toJSON() + + if (!doc.cursors[id]) { + doc.cursors[id] = { + key: id, + type: 'collaborative_selection' + } + } + + const cursor = doc.cursors[id] + const { focus, anchor } = operation.newProperties + + if (focus) cursor.focus = focus + if (anchor) cursor.anchor = anchor + + return doc +} + const setValue = (doc, op) => doc const opType: any = { @@ -18,13 +42,16 @@ const opType: any = { set_value: setValue } -export const applyOperation = (doc: SyncDoc, op: Operation): SyncDoc => { +export const applyOperation = meta => ( + doc: SyncDoc, + op: Operation +): SyncDoc => { try { const applyOp = opType[op.type] if (!applyOp) throw new TypeError('Invalid operation type!') - return applyOp(doc, op) + return applyOp(doc, op, meta) } catch (e) { console.error(e) @@ -32,5 +59,5 @@ export const applyOperation = (doc: SyncDoc, op: Operation): SyncDoc => { } } -export const applySlateOps = (doc: SyncDoc, operations: Operations) => - operations.reduce(applyOperation, doc) +export const applySlateOps = (doc: SyncDoc, operations: Operations, meta) => + operations.reduce(applyOperation(meta), doc) diff --git a/packages/bridge/src/convert/insert.ts b/packages/bridge/src/convert/insert.ts index fd2a508..dc7a300 100644 --- a/packages/bridge/src/convert/insert.ts +++ b/packages/bridge/src/convert/insert.ts @@ -15,18 +15,20 @@ const insertNodeOp = ({ value, obj, index, path }: Automerge.Diff) => map => { const inserate = ({ nodes, ...json }: any, path) => { const node = nodes ? { ...json, nodes: [] } : json - if (node.object === 'mark') { - ops.push({ - type: 'add_mark', - path: path.slice(0, -1), - mark: node - }) - } else { - ops.push({ - type: 'insert_node', - path, - node - }) + if (node.object) { + if (node.object === 'mark') { + ops.push({ + type: 'add_mark', + path: path.slice(0, -1), + mark: node + }) + } else { + ops.push({ + type: 'insert_node', + path, + node + }) + } } nodes && nodes.forEach((n, i) => inserate(n, [...path, i])) diff --git a/packages/bridge/src/convert/set.ts b/packages/bridge/src/convert/set.ts index 8189597..26d4c98 100644 --- a/packages/bridge/src/convert/set.ts +++ b/packages/bridge/src/convert/set.ts @@ -21,7 +21,7 @@ const opSet = (op: Automerge.Diff, [map, ops]) => { if (set && path) { ops.push(set(op)) - } else { + } else if (map[obj]) { map[obj][key] = link ? map[value] : value } diff --git a/packages/bridge/src/utils/index.ts b/packages/bridge/src/utils/index.ts index e33660e..7dc3915 100644 --- a/packages/bridge/src/utils/index.ts +++ b/packages/bridge/src/utils/index.ts @@ -1,7 +1,14 @@ import toSync from './toSync' import hexGen from './hexGen' -export const toJS = node => JSON.parse(JSON.stringify(node)) +export const toJS = node => { + try { + return JSON.parse(JSON.stringify(node)) + } catch (e) { + console.error(e) + return null + } +} export const cloneNode = node => toSync(toJS(node)) diff --git a/packages/client/src/Connection.ts b/packages/client/src/Connection.ts index 6971d33..a9374d7 100644 --- a/packages/client/src/Connection.ts +++ b/packages/client/src/Connection.ts @@ -5,7 +5,12 @@ import io from 'socket.io-client' import { Value, Operation } from 'slate' import { ConnectionModel } from './model' -import { applySlateOps, toSlateOp, toJS } from '@slate-collaborative/bridge' +import { + applySlateOps, + toSlateOp, + hexGen, + toJS +} from '@slate-collaborative/bridge' class Connection { url: string @@ -15,6 +20,7 @@ class Connection { socket: SocketIOClient.Socket editor: any connectOpts: any + selection: any onConnect?: () => void onDisconnect?: () => void @@ -68,13 +74,47 @@ class Connection { }) }) - setTimeout(() => (this.editor.remote = false), 5) + await Promise.resolve() + + this.editor.remote = false + + this.setCursors(docNew.cursors) } } catch (e) { console.error(e) } } + setCursors = cursors => { + if (!cursors) return + // console.log('setCursors', cursors) + const { + value: { annotations } + } = this.editor + + const keyMap = {} + + this.editor.withoutSaving(() => { + annotations.forEach(anno => { + // if (cursors[anno.key]) { + // console.log('set cursor', anno, ) + // this.editor.setAnnotation(anno, cursors[anno.key]) + // keyMap[anno.key] = true + // } else { + // this.editor.removeAnnotation(anno) + // } + + this.editor.removeAnnotation(anno) + }) + + Object.keys(cursors).forEach(key => { + if (key !== this.socket.id && !keyMap[key]) { + this.editor.addAnnotation(cursors[key]) + } + }) + }) + } + receiveSlateOps = (operations: Immutable.List) => { const doc = this.docSet.getDoc(this.docId) const message = `change from ${this.socket.id}` @@ -82,7 +122,10 @@ class Connection { if (!doc) return const changed = Automerge.change(doc, message, (d: any) => - applySlateOps(d, operations) + applySlateOps(d, operations, { + id: this.socket.id, + selection: this.editor.value.selection + }) ) this.docSet.setDoc(this.docId, changed) diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index cd0dc30..38f1b0d 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -2,6 +2,7 @@ import { ReactNode } from 'react' import onChange from './onChange' import renderEditor from './renderEditor' +import renderAnnotation from './renderAnnotation' import Connection from './Connection' @@ -22,7 +23,8 @@ const plugin = (opts: PluginOptions = {}) => { return { onChange: onChange(options), - renderEditor: renderEditor(options) + renderEditor: renderEditor(options), + renderAnnotation } } diff --git a/packages/client/src/onChange.ts b/packages/client/src/onChange.ts index a71a371..3632c16 100644 --- a/packages/client/src/onChange.ts +++ b/packages/client/src/onChange.ts @@ -1,7 +1,7 @@ import { ExtendedEditor } from './model' const onChange = opts => (editor: ExtendedEditor, next: () => void) => { - if (!editor.remote) { + if (editor.connection && !editor.remote) { const operations: any = editor.operations editor.connection.receiveSlateOps(operations) diff --git a/packages/client/src/renderAnnotation.tsx b/packages/client/src/renderAnnotation.tsx new file mode 100644 index 0000000..3929ee0 --- /dev/null +++ b/packages/client/src/renderAnnotation.tsx @@ -0,0 +1,43 @@ +import React from 'react' + +const wrapStyles = { backgroundColor: '#e91e63', position: 'relative' } + +const cursorStyleBase = { + position: 'absolute', + top: 0, + pointerEvents: 'none', + userSelect: 'none', + transform: 'translateY(-100%)', + fontSize: '10px', + color: 'white', + background: 'palevioletred', + whiteSpace: 'nowrap' +} as any + +const renderAnnotation = (props, editor, next) => { + const { children, annotation, attributes } = props + + const isLeft = annotation.focus.offset >= annotation.anchor.offset + + console.log('isLeft', isLeft) + + const cursorStyles = { ...cursorStyleBase, left: isLeft ? '0%' : '100%' } + + console.log('renderAnnotation', annotation.toJSON()) + + switch (annotation.type) { + case 'collaborative_selection': + return ( + + + {annotation.key} + + {children} + + ) + default: + return next() + } +} + +export default renderAnnotation