From 2a4d64b0746e50367e01077667cf8904d087b8d5 Mon Sep 17 00:00:00 2001 From: cudr Date: Thu, 10 Oct 2019 22:45:31 +0300 Subject: [PATCH 1/7] feat: add test annotations --- packages/backend/src/Connection.ts | 2 + packages/bridge/src/apply/annotation.ts | 3 ++ packages/bridge/src/apply/index.ts | 37 +++++++++++++++--- packages/bridge/src/convert/insert.ts | 26 +++++++------ packages/bridge/src/convert/set.ts | 2 +- packages/bridge/src/utils/index.ts | 9 ++++- packages/client/src/Connection.ts | 49 ++++++++++++++++++++++-- packages/client/src/index.ts | 4 +- packages/client/src/onChange.ts | 2 +- packages/client/src/renderAnnotation.tsx | 43 +++++++++++++++++++++ 10 files changed, 153 insertions(+), 24 deletions(-) create mode 100644 packages/client/src/renderAnnotation.tsx 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 From 4efc8db60ee076ccf4d059d4b2656787dd89590a Mon Sep 17 00:00:00 2001 From: cudr Date: Sat, 12 Oct 2019 08:02:37 +0300 Subject: [PATCH 2/7] feat: success show cursor position --- packages/backend/src/Connection.ts | 14 +++++-- packages/bridge/src/apply/annotation.ts | 6 +-- packages/bridge/src/apply/index.ts | 36 +++------------- packages/bridge/src/apply/selection.ts | 35 ++++++++++++++++ packages/client/src/Connection.ts | 53 +++++++++++++++++------- packages/client/src/renderAnnotation.tsx | 23 ++++++---- 6 files changed, 109 insertions(+), 58 deletions(-) create mode 100644 packages/bridge/src/apply/selection.ts diff --git a/packages/backend/src/Connection.ts b/packages/backend/src/Connection.ts index 3e88218..3b6b8e2 100644 --- a/packages/backend/src/Connection.ts +++ b/packages/backend/src/Connection.ts @@ -40,10 +40,18 @@ class Connection { } private saveDoc = throttle(pathname => { - if (this.options.onDocumentSave) { - const doc = this.docSet.getDoc(pathname) + try { + if (this.options.onDocumentSave) { + const doc = this.docSet.getDoc(pathname) - this.options.onDocumentSave(pathname, toJS(doc)) + const data = toJS(doc) + + delete data.cursors + + this.options.onDocumentSave(pathname, data) + } + } catch (e) { + console.log(e) } }, (this.options && this.options.saveTreshold) || 2000) diff --git a/packages/bridge/src/apply/annotation.ts b/packages/bridge/src/apply/annotation.ts index 8650aed..e58bb85 100644 --- a/packages/bridge/src/apply/annotation.ts +++ b/packages/bridge/src/apply/annotation.ts @@ -1,17 +1,17 @@ import { Operation, SyncDoc } from '../model' export const addAnnotation = (doc: SyncDoc, op: Operation): SyncDoc => { - console.log('addAnnotation!!!', op) + console.log('addAnnotation!!!', op.toJS()) return doc } export const removeAnnotation = (doc: SyncDoc, op: Operation): SyncDoc => { - console.log('removeAnnotation!!!', op) + console.log('removeAnnotation!!!', op.toJS()) return doc } export const setAnnotation = (doc: SyncDoc, op: Operation): SyncDoc => { - console.log('setAnnotation!!!', op) + console.log('setAnnotation!!!', op.toJS()) return doc } diff --git a/packages/bridge/src/apply/index.ts b/packages/bridge/src/apply/index.ts index 1a19ae1..fdd3983 100644 --- a/packages/bridge/src/apply/index.ts +++ b/packages/bridge/src/apply/index.ts @@ -3,43 +3,19 @@ import { Operation, Operations, SyncDoc } from '../model' import node from './node' import mark from './mark' import text from './text' +import selection from './selection' import annotation from './annotation' -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 setSelection = doc => doc +const setValue = doc => doc const opType: any = { ...text, ...annotation, ...node, ...mark, - - set_selection: setSelection, - set_value: setValue + ...selection + // set_value: setValue } export const applyOperation = meta => ( @@ -49,7 +25,7 @@ export const applyOperation = meta => ( try { const applyOp = opType[op.type] - if (!applyOp) throw new TypeError('Invalid operation type!') + if (!applyOp) throw new TypeError('Unsupported operation type!') return applyOp(doc, op, meta) } catch (e) { diff --git a/packages/bridge/src/apply/selection.ts b/packages/bridge/src/apply/selection.ts new file mode 100644 index 0000000..82e2359 --- /dev/null +++ b/packages/bridge/src/apply/selection.ts @@ -0,0 +1,35 @@ +import { toJS } from '../utils' + +const setSelection = (doc, op, { id, selection, annotationType }) => { + if (!doc.cursors) { + doc.cursors = {} + } + + const operation = op.toJS() + + if (!doc.cursors[id]) { + doc.cursors[id] = { + key: id, + type: annotationType, + data: {} + } + } + + const cursor = doc.cursors[id] + const { focus, anchor } = operation.newProperties + + if (focus) cursor.focus = focus + if (anchor) cursor.anchor = anchor + + const cursorPath = (anchor && anchor.path) || (focus && focus.path) + + if (cursorPath) cursor.data.cursorPath = toJS(cursorPath) + + cursor.data.isBackward = selection.isBackward + + return doc +} + +export default { + set_selection: setSelection +} diff --git a/packages/client/src/Connection.ts b/packages/client/src/Connection.ts index a9374d7..427efba 100644 --- a/packages/client/src/Connection.ts +++ b/packages/client/src/Connection.ts @@ -87,29 +87,23 @@ class Connection { setCursors = cursors => { if (!cursors) return - // console.log('setCursors', cursors) + const { value: { annotations } } = this.editor const keyMap = {} + console.log('cursors', cursors) + 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]) + this.editor.addAnnotation(toJS(cursors[key])) } }) }) @@ -121,11 +115,42 @@ class Connection { if (!doc) return + const selectionOps = operations.filter(op => op.type === 'set_selection') + + console.log('hasSelectionOps', selectionOps.size) + + const { value } = this.editor + + const { selection } = value + + const meta = { + id: this.socket.id, + selection, + annotationType: 'collaborative_selection' + } + + const cursor = doc.cursors[meta.id] + const cursorOffset = cursor && cursor.anchor && cursor.anchor.offset + + if (!selectionOps.size && selection.start.offset !== cursorOffset) { + const opData = { + type: 'set_selection', + properties: {}, + newProperties: { + anchor: selection.start, + focus: selection.end + } + } + + const op = Operation.fromJSON(opData) + + operations = operations.push(op) + } + + console.log('operations', operations.toJSON()) + const changed = Automerge.change(doc, message, (d: any) => - applySlateOps(d, operations, { - id: this.socket.id, - selection: this.editor.value.selection - }) + applySlateOps(d, operations, meta) ) this.docSet.setDoc(this.docId, changed) diff --git a/packages/client/src/renderAnnotation.tsx b/packages/client/src/renderAnnotation.tsx index 3929ee0..ce7cf97 100644 --- a/packages/client/src/renderAnnotation.tsx +++ b/packages/client/src/renderAnnotation.tsx @@ -15,23 +15,30 @@ const cursorStyleBase = { } as any const renderAnnotation = (props, editor, next) => { - const { children, annotation, attributes } = props + const { children, annotation, attributes, node } = props - const isLeft = annotation.focus.offset >= annotation.anchor.offset + const isBackward = annotation.data.get('isBackward') + const cursorPath = annotation.data.get('cursorPath') - console.log('isLeft', isLeft) + const cursorStyles = { ...cursorStyleBase, left: isBackward ? '0%' : '100%' } - const cursorStyles = { ...cursorStyleBase, left: isLeft ? '0%' : '100%' } + const { document } = editor.value - console.log('renderAnnotation', annotation.toJSON()) + const targetNode = document.getNode(cursorPath) + + const isTarget = targetNode && targetNode.key === node.key + + const showCursor = isTarget switch (annotation.type) { case 'collaborative_selection': return ( - - {annotation.key} - + {showCursor ? ( + + {annotation.key} + + ) : null} {children} ) From ad2780b936143648cd7d029ad2ba9939d3699bac Mon Sep 17 00:00:00 2001 From: cudr Date: Sat, 12 Oct 2019 12:19:06 +0300 Subject: [PATCH 3/7] feat: success garbage cursor --- packages/backend/src/Connection.ts | 15 +++++++++++++++ packages/bridge/src/apply/selection.ts | 4 +++- packages/client/src/Connection.ts | 24 ++++++++++++++++-------- 3 files changed, 34 insertions(+), 9 deletions(-) diff --git a/packages/backend/src/Connection.ts b/packages/backend/src/Connection.ts index 3b6b8e2..cd72149 100644 --- a/packages/backend/src/Connection.ts +++ b/packages/backend/src/Connection.ts @@ -124,6 +124,7 @@ class Connection { socket.leave(id) + this.garbageCursor(socket.nsp.name, id) this.garbageNsp() } @@ -137,6 +138,20 @@ class Connection { }) } + garbageCursor = (nsp, id) => { + const doc = this.docSet.getDoc(nsp) + + if (!doc.cursors) return + + const change = Automerge.change( + doc, + `remove cursor ${id}`, + (d: any) => delete d.cursors[id] + ) + + this.docSet.setDoc(nsp, change) + } + removeDoc = async nsp => { const doc = this.docSet.getDoc(nsp) diff --git a/packages/bridge/src/apply/selection.ts b/packages/bridge/src/apply/selection.ts index 82e2359..55283bd 100644 --- a/packages/bridge/src/apply/selection.ts +++ b/packages/bridge/src/apply/selection.ts @@ -21,7 +21,9 @@ const setSelection = (doc, op, { id, selection, annotationType }) => { if (focus) cursor.focus = focus if (anchor) cursor.anchor = anchor - const cursorPath = (anchor && anchor.path) || (focus && focus.path) + const cursorPath = cursor.data.isBackward + ? anchor && anchor.path + : focus && focus.path if (cursorPath) cursor.data.cursorPath = toJS(cursorPath) diff --git a/packages/client/src/Connection.ts b/packages/client/src/Connection.ts index 427efba..63b7496 100644 --- a/packages/client/src/Connection.ts +++ b/packages/client/src/Connection.ts @@ -115,9 +115,7 @@ class Connection { if (!doc) return - const selectionOps = operations.filter(op => op.type === 'set_selection') - - console.log('hasSelectionOps', selectionOps.size) + operations = operations.filter(op => op.type !== 'set_selection') const { value } = this.editor @@ -130,12 +128,24 @@ class Connection { } const cursor = doc.cursors[meta.id] - const cursorOffset = cursor && cursor.anchor && cursor.anchor.offset + const cursorStart = cursor && cursor.anchor && cursor.anchor.offset + const cursorEnd = cursor && cursor.focus && cursor.focus.offset - if (!selectionOps.size && selection.start.offset !== cursorOffset) { + console.log('cursor!!', cursorStart, cursorEnd) + console.log('selection!!', selection.start.offset, selection.end.offset) + + if ( + selection.start.offset !== cursorStart || + selection.end.offset !== cursorEnd + ) { + console.log( + '!!!!!! append selection op!!!', + selection.start.toJS(), + selection.end.toJS() + ) const opData = { type: 'set_selection', - properties: {}, + properties: cursor || {}, newProperties: { anchor: selection.start, focus: selection.end @@ -147,8 +157,6 @@ class Connection { operations = operations.push(op) } - console.log('operations', operations.toJSON()) - const changed = Automerge.change(doc, message, (d: any) => applySlateOps(d, operations, meta) ) From 55def80703584964234b6fe8c5a73a3c2f05352f Mon Sep 17 00:00:00 2001 From: cudr Date: Sat, 12 Oct 2019 20:58:23 +0300 Subject: [PATCH 4/7] feat: try to use inner annotations model to cursors --- packages/bridge/src/apply/index.ts | 14 ++-- packages/bridge/src/apply/selection.ts | 37 ---------- packages/bridge/src/convert/index.ts | 6 +- packages/bridge/src/convert/insert.ts | 2 +- packages/bridge/src/convert/remove.ts | 10 ++- packages/bridge/src/convert/set.ts | 32 +++++++++ packages/bridge/src/cursor/index.ts | 88 +++++++++++++++++++++++ packages/bridge/src/index.ts | 1 + packages/client/src/Connection.ts | 92 ++++++++++-------------- packages/client/src/model.ts | 20 ++++-- packages/client/src/renderAnnotation.tsx | 5 +- 11 files changed, 196 insertions(+), 111 deletions(-) delete mode 100644 packages/bridge/src/apply/selection.ts create mode 100644 packages/bridge/src/cursor/index.ts diff --git a/packages/bridge/src/apply/index.ts b/packages/bridge/src/apply/index.ts index fdd3983..c7d366c 100644 --- a/packages/bridge/src/apply/index.ts +++ b/packages/bridge/src/apply/index.ts @@ -3,7 +3,6 @@ import { Operation, Operations, SyncDoc } from '../model' import node from './node' import mark from './mark' import text from './text' -import selection from './selection' import annotation from './annotation' const setSelection = doc => doc @@ -14,20 +13,17 @@ const opType: any = { ...annotation, ...node, ...mark, - ...selection + set_selection: setSelection // set_value: setValue } -export const applyOperation = meta => ( - doc: SyncDoc, - op: Operation -): SyncDoc => { +export const applyOperation = (doc: SyncDoc, op: Operation): SyncDoc => { try { const applyOp = opType[op.type] if (!applyOp) throw new TypeError('Unsupported operation type!') - return applyOp(doc, op, meta) + return applyOp(doc, op) } catch (e) { console.error(e) @@ -35,5 +31,5 @@ export const applyOperation = meta => ( } } -export const applySlateOps = (doc: SyncDoc, operations: Operations, meta) => - operations.reduce(applyOperation(meta), doc) +export const applySlateOps = (doc: SyncDoc, operations: Operations) => + operations.reduce(applyOperation, doc) diff --git a/packages/bridge/src/apply/selection.ts b/packages/bridge/src/apply/selection.ts deleted file mode 100644 index 55283bd..0000000 --- a/packages/bridge/src/apply/selection.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { toJS } from '../utils' - -const setSelection = (doc, op, { id, selection, annotationType }) => { - if (!doc.cursors) { - doc.cursors = {} - } - - const operation = op.toJS() - - if (!doc.cursors[id]) { - doc.cursors[id] = { - key: id, - type: annotationType, - data: {} - } - } - - const cursor = doc.cursors[id] - const { focus, anchor } = operation.newProperties - - if (focus) cursor.focus = focus - if (anchor) cursor.anchor = anchor - - const cursorPath = cursor.data.isBackward - ? anchor && anchor.path - : focus && focus.path - - if (cursorPath) cursor.data.cursorPath = toJS(cursorPath) - - cursor.data.isBackward = selection.isBackward - - return doc -} - -export default { - set_selection: setSelection -} diff --git a/packages/bridge/src/convert/index.ts b/packages/bridge/src/convert/index.ts index 7dc0321..31bd8a9 100644 --- a/packages/bridge/src/convert/index.ts +++ b/packages/bridge/src/convert/index.ts @@ -30,7 +30,11 @@ const toSlateOp = (ops: Automerge.Diff[], currentTree) => { [] ]) - return defer.flatMap(op => op(tempTree, currentTree)) + const res = defer.flatMap(op => op(tempTree, currentTree)) + + console.log('toSlate@@@', ops, res) + + return res } export { toSlateOp } diff --git a/packages/bridge/src/convert/insert.ts b/packages/bridge/src/convert/insert.ts index dc7a300..2f923d8 100644 --- a/packages/bridge/src/convert/insert.ts +++ b/packages/bridge/src/convert/insert.ts @@ -52,7 +52,7 @@ const opInsert = (op: Automerge.Diff, [map, ops]) => { if (link && map[obj]) { map[obj].splice(index, 0, map[value] || value) - } else if (type === 'text' && !path) { + } else if ((type === 'text' || type === 'list') && !path) { map[obj] = map[obj] ? map[obj] .slice(0, index) diff --git a/packages/bridge/src/convert/remove.ts b/packages/bridge/src/convert/remove.ts index a768cdf..80cd113 100644 --- a/packages/bridge/src/convert/remove.ts +++ b/packages/bridge/src/convert/remove.ts @@ -40,10 +40,18 @@ const removeNodesOp = ({ index, obj, path }: Automerge.Diff) => (map, doc) => { } } +const removeAnnotationOp = ({ key }: Automerge.Diff) => (map, doc) => { + return { + type: 'remove_annotation', + annotation: toJS(doc.annotations[key]) + } +} + const removeByType = { text: removeTextOp, nodes: removeNodesOp, - marks: removeMarkOp + marks: removeMarkOp, + annotations: removeAnnotationOp } const opRemove = (op: Automerge.Diff, [map, ops]) => { diff --git a/packages/bridge/src/convert/set.ts b/packages/bridge/src/convert/set.ts index 26d4c98..9aa3d85 100644 --- a/packages/bridge/src/convert/set.ts +++ b/packages/bridge/src/convert/set.ts @@ -10,6 +10,31 @@ const setDataOp = ({ path, value }: Automerge.Diff) => map => ({ } }) +const AnnotationSetOp = ({ key, value }: Automerge.Diff) => (map, doc) => { + if (!doc.annotations) { + doc.annotations = {} + } + + let op + + if (!doc.annotations[key]) { + op = { + type: 'add_annotation', + annotation: map[value] + } + } else { + op = { + type: 'set_annotation', + properties: doc.annotations[key], + newProperties: map[value] + } + } + + console.log('opSET!!', key, map[value], op) + + return op +} + const setByType = { data: setDataOp } @@ -25,6 +50,13 @@ const opSet = (op: Automerge.Diff, [map, ops]) => { map[obj][key] = link ? map[value] : value } + /** + * Annotation + */ + if (path && path.length === 1 && path[0] === 'annotations') { + ops.push(AnnotationSetOp(op)) + } + return [map, ops] } catch (e) { console.error(e, op, toJS(map)) diff --git a/packages/bridge/src/cursor/index.ts b/packages/bridge/src/cursor/index.ts new file mode 100644 index 0000000..c0eeee5 --- /dev/null +++ b/packages/bridge/src/cursor/index.ts @@ -0,0 +1,88 @@ +import { toJS } from '../utils' + +import { Operation } from 'slate' +import { List } from 'immutable' + +export const setCursor = ( + doc, + { id, selection, selectionOps, annotationType } +) => { + if (!doc.annotations) { + doc.annotations = {} + } + + if (!doc.annotations[id]) { + doc.annotations[id] = { + key: id, + type: annotationType, + data: {} + } + } + + const annotation = toJS(doc.annotations[id]) + + if (selectionOps.size) { + selectionOps.forEach(op => { + const { newProperties } = op.toJSON() + + if (newProperties.focus) annotation.focus = newProperties.focus + if (newProperties.anchor) annotation.anchor = newProperties.anchor + if (newProperties.data) annotation.data = newProperties.data + }) + } + + const cursorStart = annotation.anchor && annotation.anchor.offset + const cursorEnd = annotation.focus && annotation.focus.offset + + console.log('cursor!!', cursorStart, cursorEnd) + console.log( + 'selection!!', + annotation, + selection.start.offset, + selection.end.offset + ) + + if (selection.start.offset !== cursorStart) { + annotation.focus = selection.end.toJS() || {} + } + + if (selection.end.offset !== cursorEnd) { + annotation.anchor = selection.start.toJS() || {} + } + + annotation.data.isBackward = selection.isBackward + + console.log('setted cursor', annotation, toJS(doc)) + + doc.annotations[id] = annotation + + return doc +} + +export const removeCursor = (doc, { id }) => { + console.log('!!!removeCursor', doc, id) + if (doc.annotations && doc.annotations[id]) { + delete doc.annotations[id] + } + + return doc +} + +export const cursorOpFilter = (doc, ops: List) => + ops.filter(op => { + const { annotations } = doc + + if (op.type === 'set_annotation') { + return !( + (op.properties && annotations[op.properties.key]) || + (op.newProperties && annotations[op.newProperties.key]) + ) + } else if ( + op.type === 'add_annotation' || + op.type === 'remove_annotation' + ) { + return !annotations[op.annotation.key] + } + + return true + }) diff --git a/packages/bridge/src/index.ts b/packages/bridge/src/index.ts index 26d4a2e..5b54441 100644 --- a/packages/bridge/src/index.ts +++ b/packages/bridge/src/index.ts @@ -1,3 +1,4 @@ export * from './apply' export * from './convert' export * from './utils' +export * from './cursor' diff --git a/packages/client/src/Connection.ts b/packages/client/src/Connection.ts index 63b7496..79312d7 100644 --- a/packages/client/src/Connection.ts +++ b/packages/client/src/Connection.ts @@ -3,12 +3,14 @@ import Immutable from 'immutable' import io from 'socket.io-client' import { Value, Operation } from 'slate' -import { ConnectionModel } from './model' +import { ConnectionModel, ExtendedEditor } from './model' import { + setCursor, + removeCursor, + cursorOpFilter, applySlateOps, toSlateOp, - hexGen, toJS } from '@slate-collaborative/bridge' @@ -18,7 +20,7 @@ class Connection { docSet: Automerge.DocSet connection: Automerge.Connection socket: SocketIOClient.Socket - editor: any + editor: ExtendedEditor connectOpts: any selection: any onConnect?: () => void @@ -56,6 +58,9 @@ class Connection { const currentDoc = this.docSet.getDoc(this.docId) const docNew = this.connection.receiveMsg(data) + console.log('current doc before updates', toJS(currentDoc)) + console.log('new doc with remote updates!!', toJS(docNew)) + if (!docNew) { return } @@ -88,25 +93,31 @@ class Connection { setCursors = cursors => { if (!cursors) return - const { - value: { annotations } - } = this.editor + // const { + // value: { annotations } + // } = this.editor - const keyMap = {} + // const keyMap = {} - console.log('cursors', cursors) + // console.log('cursors', cursors) - this.editor.withoutSaving(() => { - annotations.forEach(anno => { - this.editor.removeAnnotation(anno) - }) + // this.editor.withoutSaving(() => { + // annotations.forEach(anno => { + // this.editor.removeAnnotation(anno) + // }) - Object.keys(cursors).forEach(key => { - if (key !== this.socket.id && !keyMap[key]) { - this.editor.addAnnotation(toJS(cursors[key])) - } - }) - }) + // Object.keys(cursors).forEach(key => { + // if (key !== this.socket.id && !keyMap[key]) { + // this.editor.addAnnotation(toJS(cursors[key])) + // } + // }) + // }) + + console.log( + '!!!!VAL', + this.connectOpts.query.name, + this.editor.value.toJSON({ preserveAnnotations: true }) + ) } receiveSlateOps = (operations: Immutable.List) => { @@ -115,52 +126,25 @@ class Connection { if (!doc) return - operations = operations.filter(op => op.type !== 'set_selection') + const { + value: { selection } + } = this.editor - const { value } = this.editor - - const { selection } = value - - const meta = { + const cursorData = { id: this.socket.id, selection, + selectionOps: operations.filter(op => op.type === 'set_selection'), annotationType: 'collaborative_selection' } - const cursor = doc.cursors[meta.id] - const cursorStart = cursor && cursor.anchor && cursor.anchor.offset - const cursorEnd = cursor && cursor.focus && cursor.focus.offset - - console.log('cursor!!', cursorStart, cursorEnd) - console.log('selection!!', selection.start.offset, selection.end.offset) - - if ( - selection.start.offset !== cursorStart || - selection.end.offset !== cursorEnd - ) { - console.log( - '!!!!!! append selection op!!!', - selection.start.toJS(), - selection.end.toJS() - ) - const opData = { - type: 'set_selection', - properties: cursor || {}, - newProperties: { - anchor: selection.start, - focus: selection.end - } - } - - const op = Operation.fromJSON(opData) - - operations = operations.push(op) - } + const withCursor = selection.isFocused ? setCursor : removeCursor const changed = Automerge.change(doc, message, (d: any) => - applySlateOps(d, operations, meta) + withCursor(applySlateOps(d, cursorOpFilter(d, operations)), cursorData) ) + console.log('doc with annotations!!', toJS(changed)) + this.docSet.setDoc(this.docId, changed) } diff --git a/packages/client/src/model.ts b/packages/client/src/model.ts index cc284c1..52b615c 100644 --- a/packages/client/src/model.ts +++ b/packages/client/src/model.ts @@ -1,16 +1,22 @@ -import { Editor } from 'slate' +import { Editor, Controller, Value } from 'slate' import { PluginOptions } from './index' import Connection from './Connection' -export interface ConnectionModel extends PluginOptions { - editor: Editor - onConnect: () => void - onDisconnect: () => void +interface FixedController extends Controller { + setValue: (value: Value) => void } export interface ExtendedEditor extends Editor { - remote: boolean - connection: Connection + remote?: boolean + connection?: Connection + controller: FixedController + setFocus: () => void +} + +export interface ConnectionModel extends PluginOptions { + editor: ExtendedEditor + onConnect: () => void + onDisconnect: () => void } export interface ControllerProps extends PluginOptions { diff --git a/packages/client/src/renderAnnotation.tsx b/packages/client/src/renderAnnotation.tsx index ce7cf97..67fd31d 100644 --- a/packages/client/src/renderAnnotation.tsx +++ b/packages/client/src/renderAnnotation.tsx @@ -18,7 +18,9 @@ const renderAnnotation = (props, editor, next) => { const { children, annotation, attributes, node } = props const isBackward = annotation.data.get('isBackward') - const cursorPath = annotation.data.get('cursorPath') + const cursorPath = isBackward ? annotation.anchor.path : annotation.focus.path + + console.log('renderAnnotation', props, isBackward, cursorPath) const cursorStyles = { ...cursorStyleBase, left: isBackward ? '0%' : '100%' } @@ -28,6 +30,7 @@ const renderAnnotation = (props, editor, next) => { const isTarget = targetNode && targetNode.key === node.key + console.log('isTarget', isTarget, targetNode, node) const showCursor = isTarget switch (annotation.type) { From 242a836ce80db79dd7a44763c519cc0c12990d4e Mon Sep 17 00:00:00 2001 From: cudr Date: Sat, 12 Oct 2019 23:15:02 +0300 Subject: [PATCH 5/7] feat: set_annotation temporary disabled --- packages/bridge/src/convert/set.ts | 25 +++++---- packages/bridge/src/cursor/index.ts | 64 ++++++++++-------------- packages/client/src/Connection.ts | 11 ++-- packages/client/src/Controller.tsx | 10 ++++ packages/client/src/renderAnnotation.tsx | 52 +++++++++++++------ 5 files changed, 94 insertions(+), 68 deletions(-) diff --git a/packages/bridge/src/convert/set.ts b/packages/bridge/src/convert/set.ts index 9aa3d85..1ec1273 100644 --- a/packages/bridge/src/convert/set.ts +++ b/packages/bridge/src/convert/set.ts @@ -17,18 +17,21 @@ const AnnotationSetOp = ({ key, value }: Automerge.Diff) => (map, doc) => { let op - if (!doc.annotations[key]) { - op = { - type: 'add_annotation', - annotation: map[value] - } - } else { - op = { - type: 'set_annotation', - properties: doc.annotations[key], - newProperties: map[value] - } + /** + * Looks like set_annotation option is broken, temporary disabled + */ + // if (!doc.annotations[key]) { + op = { + type: 'add_annotation', + annotation: map[value] } + // } else { + // op = { + // type: 'set_annotation', + // properties: toJS(doc.annotations[key]), + // newProperties: map[value] + // } + // } console.log('opSET!!', key, map[value], op) diff --git a/packages/bridge/src/cursor/index.ts b/packages/bridge/src/cursor/index.ts index c0eeee5..6273e43 100644 --- a/packages/bridge/src/cursor/index.ts +++ b/packages/bridge/src/cursor/index.ts @@ -3,10 +3,7 @@ import { toJS } from '../utils' import { Operation } from 'slate' import { List } from 'immutable' -export const setCursor = ( - doc, - { id, selection, selectionOps, annotationType } -) => { +export const setCursor = (doc, { id, selection, annotationType }) => { if (!doc.annotations) { doc.annotations = {} } @@ -21,38 +18,31 @@ export const setCursor = ( const annotation = toJS(doc.annotations[id]) - if (selectionOps.size) { - selectionOps.forEach(op => { - const { newProperties } = op.toJSON() + // if (selectionOps.size) { + // selectionOps.forEach(op => { + // const { newProperties } = op.toJSON() - if (newProperties.focus) annotation.focus = newProperties.focus - if (newProperties.anchor) annotation.anchor = newProperties.anchor - if (newProperties.data) annotation.data = newProperties.data - }) - } + // if (newProperties.focus) annotation.focus = newProperties.focus + // if (newProperties.anchor) annotation.anchor = newProperties.anchor + // if (newProperties.data) annotation.data = newProperties.data + // }) + // } - const cursorStart = annotation.anchor && annotation.anchor.offset - const cursorEnd = annotation.focus && annotation.focus.offset + // console.log('cursor!!', cursorStart, cursorEnd) + // console.log( + // 'selection!!', + // selection.toJSON(), + // selection.start.offset, + // selection.end.offset + // ) - console.log('cursor!!', cursorStart, cursorEnd) - console.log( - 'selection!!', - annotation, - selection.start.offset, - selection.end.offset - ) - - if (selection.start.offset !== cursorStart) { - annotation.focus = selection.end.toJS() || {} - } - - if (selection.end.offset !== cursorEnd) { - annotation.anchor = selection.start.toJS() || {} - } + annotation.focus = selection.end.toJSON() || {} + annotation.anchor = selection.start.toJSON() || {} annotation.data.isBackward = selection.isBackward - - console.log('setted cursor', annotation, toJS(doc)) + annotation.data.targetPath = selection.isBackward + ? annotation.anchor.path + : annotation.focus.path doc.annotations[id] = annotation @@ -60,7 +50,7 @@ export const setCursor = ( } export const removeCursor = (doc, { id }) => { - console.log('!!!removeCursor', doc, id) + // console.log('!!!removeCursor', doc, id) if (doc.annotations && doc.annotations[id]) { delete doc.annotations[id] } @@ -68,20 +58,18 @@ export const removeCursor = (doc, { id }) => { return doc } -export const cursorOpFilter = (doc, ops: List) => +export const cursorOpFilter = (ops: List, annotationType) => ops.filter(op => { - const { annotations } = doc - if (op.type === 'set_annotation') { return !( - (op.properties && annotations[op.properties.key]) || - (op.newProperties && annotations[op.newProperties.key]) + (op.properties && op.properties.type === annotationType) || + (op.newProperties && op.newProperties.type === annotationType) ) } else if ( op.type === 'add_annotation' || op.type === 'remove_annotation' ) { - return !annotations[op.annotation.key] + return op.annotation.type !== annotationType } return true diff --git a/packages/client/src/Connection.ts b/packages/client/src/Connection.ts index 79312d7..bec6626 100644 --- a/packages/client/src/Connection.ts +++ b/packages/client/src/Connection.ts @@ -130,17 +130,22 @@ class Connection { value: { selection } } = this.editor + const annotationType = 'collaborative_selection' + const cursorData = { id: this.socket.id, selection, - selectionOps: operations.filter(op => op.type === 'set_selection'), - annotationType: 'collaborative_selection' + // selectionOps: operations.filter(op => op.type === 'set_selection'), + annotationType } const withCursor = selection.isFocused ? setCursor : removeCursor const changed = Automerge.change(doc, message, (d: any) => - withCursor(applySlateOps(d, cursorOpFilter(d, operations)), cursorData) + withCursor( + applySlateOps(d, cursorOpFilter(operations, annotationType)), + cursorData + ) ) console.log('doc with annotations!!', toJS(changed)) diff --git a/packages/client/src/Controller.tsx b/packages/client/src/Controller.tsx index da41219..d5b4bbf 100644 --- a/packages/client/src/Controller.tsx +++ b/packages/client/src/Controller.tsx @@ -26,6 +26,16 @@ class Controller extends Component { onConnect: this.onConnect, onDisconnect: this.onDisconnect }) + + //@ts-ignore + if (!window.counter) { + //@ts-ignore + window.counter = 1 + } + //@ts-ignore + window[`Editor_${counter}`] = editor + //@ts-ignore + window.counter += 1 } componentWillUnmount() { diff --git a/packages/client/src/renderAnnotation.tsx b/packages/client/src/renderAnnotation.tsx index 67fd31d..6c342c2 100644 --- a/packages/client/src/renderAnnotation.tsx +++ b/packages/client/src/renderAnnotation.tsx @@ -1,46 +1,66 @@ -import React from 'react' +import React, { Fragment } from 'react' -const wrapStyles = { backgroundColor: '#e91e63', position: 'relative' } +const wrapStyles = { + backgroundColor: 'rgba(233, 30, 99, 0.2)', + position: 'relative' +} const cursorStyleBase = { position: 'absolute', - top: 0, + top: -2, pointerEvents: 'none', userSelect: 'none', transform: 'translateY(-100%)', - fontSize: '10px', + fontSize: 10, color: 'white', background: 'palevioletred', whiteSpace: 'nowrap' } as any +const caretStyleBase = { + position: 'absolute', + top: 0, + pointerEvents: 'none', + userSelect: 'none', + height: '100%', + width: 2, + background: '#bf1b52' +} + const renderAnnotation = (props, editor, next) => { const { children, annotation, attributes, node } = props const isBackward = annotation.data.get('isBackward') - const cursorPath = isBackward ? annotation.anchor.path : annotation.focus.path + const targetPath = annotation.data.get('targetPath') - console.log('renderAnnotation', props, isBackward, cursorPath) + console.log( + 'renderAnnotation', + annotation.toJS(), + props, + isBackward, + targetPath + ) - const cursorStyles = { ...cursorStyleBase, left: isBackward ? '0%' : '100%' } + const badgeStyles = { ...cursorStyleBase, left: isBackward ? '0%' : '100%' } + const caretStyles = { ...caretStyleBase, left: isBackward ? '0%' : '100%' } const { document } = editor.value - const targetNode = document.getNode(cursorPath) + const targetNode = document.getNode(targetPath) - const isTarget = targetNode && targetNode.key === node.key - - console.log('isTarget', isTarget, targetNode, node) - const showCursor = isTarget + const isShowCursor = targetNode && targetNode.key === node.key switch (annotation.type) { case 'collaborative_selection': return ( - {showCursor ? ( - - {annotation.key} - + {isShowCursor ? ( + + + {annotation.key} + + + ) : null} {children} From 0ceb38bbfd26d90e2b712ea450badbdb8b5c7b52 Mon Sep 17 00:00:00 2001 From: cudr Date: Sun, 13 Oct 2019 19:37:56 +0300 Subject: [PATCH 6/7] feat: cursors should works with annotations --- packages/backend/src/Connection.ts | 44 ++++++++++---- packages/backend/src/model.ts | 1 + packages/backend/src/utils/index.ts | 3 +- packages/bridge/src/convert/index.ts | 6 +- packages/bridge/src/convert/remove.ts | 10 ++- packages/bridge/src/convert/set.ts | 2 - packages/bridge/src/cursor/index.ts | 77 +++++++++++------------- packages/bridge/src/index.ts | 1 + packages/bridge/src/model/automerge.ts | 4 +- packages/bridge/src/utils/index.ts | 2 +- packages/client/Cursor.tsx | 0 packages/client/src/Connection.ts | 68 ++++++--------------- packages/client/src/Controller.tsx | 25 ++++---- packages/client/src/index.ts | 34 ++++++----- packages/client/src/model.ts | 20 +++++- packages/client/src/renderAnnotation.tsx | 69 +++++++++++---------- packages/client/src/renderEditor.tsx | 3 +- packages/example/package.json | 1 + packages/example/src/Client.tsx | 30 +++++++-- 19 files changed, 214 insertions(+), 186 deletions(-) create mode 100644 packages/client/Cursor.tsx diff --git a/packages/backend/src/Connection.ts b/packages/backend/src/Connection.ts index cd72149..aa27f4e 100644 --- a/packages/backend/src/Connection.ts +++ b/packages/backend/src/Connection.ts @@ -2,6 +2,7 @@ import io from 'socket.io' import { ValueJSON } from 'slate' import * as Automerge from 'automerge' import throttle from 'lodash/throttle' +import merge from 'lodash/merge' import { toSync, toJS } from '@slate-collaborative/bridge' @@ -18,7 +19,7 @@ class Connection { this.io = io(options.port, options.connectOpts) this.docSet = new Automerge.DocSet() this.connections = {} - this.options = options + this.options = merge(defaultOptions, options) this.configure() } @@ -32,7 +33,7 @@ class Connection { private appendDoc = (path: string, value: ValueJSON) => { const sync = toSync(value) - sync.cursors = {} + sync.annotations = {} const doc = Automerge.from(sync) @@ -44,11 +45,13 @@ class Connection { if (this.options.onDocumentSave) { const doc = this.docSet.getDoc(pathname) - const data = toJS(doc) + if (doc) { + const data = toJS(doc) - delete data.cursors + delete data.annotations - this.options.onDocumentSave(pathname, data) + this.options.onDocumentSave(pathname, data) + } } } catch (e) { console.log(e) @@ -106,6 +109,8 @@ class Connection { socket.on('operation', this.onOperation(id, name)) socket.on('disconnect', this.onDisconnect(id, socket)) + + this.garbageCursors(name) } private onOperation = (id, name) => data => { @@ -125,6 +130,8 @@ class Connection { socket.leave(id) this.garbageCursor(socket.nsp.name, id) + this.garbageCursors(socket.nsp.name) + this.garbageNsp() } @@ -141,17 +148,32 @@ class Connection { garbageCursor = (nsp, id) => { const doc = this.docSet.getDoc(nsp) - if (!doc.cursors) return + if (!doc.annotations) return - const change = Automerge.change( - doc, - `remove cursor ${id}`, - (d: any) => delete d.cursors[id] - ) + const change = Automerge.change(doc, `remove cursor ${id}`, (d: any) => { + delete d.annotations[id] + }) this.docSet.setDoc(nsp, change) } + garbageCursors = nsp => { + const doc = this.docSet.getDoc(nsp) + + if (!doc.annotations) return + + const namespace = this.io.of(nsp) + + Object.keys(doc.annotations).forEach(key => { + if ( + !namespace.sockets[key] && + doc.annotations[key].type === this.options.cursorAnnotationType + ) { + this.garbageCursor(nsp, key) + } + }) + } + removeDoc = async nsp => { const doc = this.docSet.getDoc(nsp) diff --git a/packages/backend/src/model.ts b/packages/backend/src/model.ts index 9f3ede9..4adcf09 100644 --- a/packages/backend/src/model.ts +++ b/packages/backend/src/model.ts @@ -5,6 +5,7 @@ export interface ConnectionOptions { connectOpts?: SocketIO.ServerOptions defaultValue?: ValueJSON saveTreshold?: number + cursorAnnotationType?: string onAuthRequest?: ( query: Object, socket?: SocketIO.Socket diff --git a/packages/backend/src/utils/index.ts b/packages/backend/src/utils/index.ts index d8567a8..5bcce0e 100644 --- a/packages/backend/src/utils/index.ts +++ b/packages/backend/src/utils/index.ts @@ -7,7 +7,8 @@ export const getClients = (io, nsp) => export const defaultOptions = { port: 9000, - saveTreshold: 2000 + saveTreshold: 2000, + cursorAnnotationType: 'collaborative_selection' } export { defaultValue } diff --git a/packages/bridge/src/convert/index.ts b/packages/bridge/src/convert/index.ts index 31bd8a9..f0bc7a3 100644 --- a/packages/bridge/src/convert/index.ts +++ b/packages/bridge/src/convert/index.ts @@ -14,7 +14,7 @@ const byAction = { const rootKey = '00000000-0000-0000-0000-000000000000' -const toSlateOp = (ops: Automerge.Diff[], currentTree) => { +const toSlateOp = (ops: Automerge.Diff[], doc) => { const iterate = (acc, op) => { const action = byAction[op.action] @@ -30,9 +30,7 @@ const toSlateOp = (ops: Automerge.Diff[], currentTree) => { [] ]) - const res = defer.flatMap(op => op(tempTree, currentTree)) - - console.log('toSlate@@@', ops, res) + const res = defer.flatMap(op => op(tempTree, doc)).filter(op => op) return res } diff --git a/packages/bridge/src/convert/remove.ts b/packages/bridge/src/convert/remove.ts index 80cd113..5b89b27 100644 --- a/packages/bridge/src/convert/remove.ts +++ b/packages/bridge/src/convert/remove.ts @@ -41,9 +41,13 @@ const removeNodesOp = ({ index, obj, path }: Automerge.Diff) => (map, doc) => { } const removeAnnotationOp = ({ key }: Automerge.Diff) => (map, doc) => { - return { - type: 'remove_annotation', - annotation: toJS(doc.annotations[key]) + const annotation = toJS(doc.annotations[key]) + + if (annotation) { + return { + type: 'remove_annotation', + annotation + } } } diff --git a/packages/bridge/src/convert/set.ts b/packages/bridge/src/convert/set.ts index 1ec1273..8927b33 100644 --- a/packages/bridge/src/convert/set.ts +++ b/packages/bridge/src/convert/set.ts @@ -33,8 +33,6 @@ const AnnotationSetOp = ({ key, value }: Automerge.Diff) => (map, doc) => { // } // } - console.log('opSET!!', key, map[value], op) - return op } diff --git a/packages/bridge/src/cursor/index.ts b/packages/bridge/src/cursor/index.ts index 6273e43..480891e 100644 --- a/packages/bridge/src/cursor/index.ts +++ b/packages/bridge/src/cursor/index.ts @@ -1,75 +1,68 @@ -import { toJS } from '../utils' - -import { Operation } from 'slate' +import { Operation, Selection } from 'slate' import { List } from 'immutable' +import merge from 'lodash/merge' + +import { toJS } from '../utils' +import { SyncDoc, CursorKey } from '../model' + +export const setCursor = ( + doc: SyncDoc, + key: CursorKey, + selection: Selection, + type, + data +) => { + if (!doc) return -export const setCursor = (doc, { id, selection, annotationType }) => { if (!doc.annotations) { doc.annotations = {} } - if (!doc.annotations[id]) { - doc.annotations[id] = { - key: id, - type: annotationType, + if (!doc.annotations[key]) { + doc.annotations[key] = { + key, + type, data: {} } } - const annotation = toJS(doc.annotations[id]) + const annotation = toJS(doc.annotations[key]) - // if (selectionOps.size) { - // selectionOps.forEach(op => { - // const { newProperties } = op.toJSON() + annotation.focus = selection.end.toJSON() + annotation.anchor = selection.start.toJSON() - // if (newProperties.focus) annotation.focus = newProperties.focus - // if (newProperties.anchor) annotation.anchor = newProperties.anchor - // if (newProperties.data) annotation.data = newProperties.data - // }) - // } + annotation.data = merge(annotation.data, data, { + isBackward: selection.isBackward, + targetPath: selection.isBackward + ? annotation.anchor.path + : annotation.focus.path + }) - // console.log('cursor!!', cursorStart, cursorEnd) - // console.log( - // 'selection!!', - // selection.toJSON(), - // selection.start.offset, - // selection.end.offset - // ) - - annotation.focus = selection.end.toJSON() || {} - annotation.anchor = selection.start.toJSON() || {} - - annotation.data.isBackward = selection.isBackward - annotation.data.targetPath = selection.isBackward - ? annotation.anchor.path - : annotation.focus.path - - doc.annotations[id] = annotation + doc.annotations[key] = annotation return doc } -export const removeCursor = (doc, { id }) => { - // console.log('!!!removeCursor', doc, id) - if (doc.annotations && doc.annotations[id]) { - delete doc.annotations[id] +export const removeCursor = (doc: SyncDoc, key: CursorKey) => { + if (doc.annotations && doc.annotations[key]) { + delete doc.annotations[key] } return doc } -export const cursorOpFilter = (ops: List, annotationType) => +export const cursorOpFilter = (ops: List, type: string) => ops.filter(op => { if (op.type === 'set_annotation') { return !( - (op.properties && op.properties.type === annotationType) || - (op.newProperties && op.newProperties.type === annotationType) + (op.properties && op.properties.type === type) || + (op.newProperties && op.newProperties.type === type) ) } else if ( op.type === 'add_annotation' || op.type === 'remove_annotation' ) { - return op.annotation.type !== annotationType + return op.annotation.type !== type } return true diff --git a/packages/bridge/src/index.ts b/packages/bridge/src/index.ts index 5b54441..8a6778a 100644 --- a/packages/bridge/src/index.ts +++ b/packages/bridge/src/index.ts @@ -2,3 +2,4 @@ export * from './apply' export * from './convert' export * from './utils' export * from './cursor' +export * from './model' diff --git a/packages/bridge/src/model/automerge.ts b/packages/bridge/src/model/automerge.ts index dce0000..82f306a 100644 --- a/packages/bridge/src/model/automerge.ts +++ b/packages/bridge/src/model/automerge.ts @@ -1,3 +1,3 @@ -import { Doc } from 'automerge' +export type CursorKey = string -export type SyncDoc = Doc +export type SyncDoc = any diff --git a/packages/bridge/src/utils/index.ts b/packages/bridge/src/utils/index.ts index 7dc3915..3029458 100644 --- a/packages/bridge/src/utils/index.ts +++ b/packages/bridge/src/utils/index.ts @@ -5,7 +5,7 @@ export const toJS = node => { try { return JSON.parse(JSON.stringify(node)) } catch (e) { - console.error(e) + console.error('Convert to js failed!!! Return null') return null } } diff --git a/packages/client/Cursor.tsx b/packages/client/Cursor.tsx new file mode 100644 index 0000000..e69de29 diff --git a/packages/client/src/Connection.ts b/packages/client/src/Connection.ts index bec6626..503240a 100644 --- a/packages/client/src/Connection.ts +++ b/packages/client/src/Connection.ts @@ -22,7 +22,8 @@ class Connection { socket: SocketIOClient.Socket editor: ExtendedEditor connectOpts: any - selection: any + annotationDataMixin: any + cursorAnnotationType: string onConnect?: () => void onDisconnect?: () => void @@ -31,11 +32,16 @@ class Connection { url, connectOpts, onConnect, - onDisconnect + onDisconnect, + cursorAnnotationType, + annotationDataMixin }: ConnectionModel) { this.url = url this.editor = editor this.connectOpts = connectOpts + this.cursorAnnotationType = cursorAnnotationType + this.annotationDataMixin = annotationDataMixin + this.onConnect = onConnect this.onDisconnect = onDisconnect @@ -58,9 +64,6 @@ class Connection { const currentDoc = this.docSet.getDoc(this.docId) const docNew = this.connection.receiveMsg(data) - console.log('current doc before updates', toJS(currentDoc)) - console.log('new doc with remote updates!!', toJS(docNew)) - if (!docNew) { return } @@ -82,44 +85,12 @@ class Connection { await Promise.resolve() this.editor.remote = false - - this.setCursors(docNew.cursors) } } catch (e) { console.error(e) } } - setCursors = cursors => { - if (!cursors) return - - // const { - // value: { annotations } - // } = this.editor - - // const keyMap = {} - - // console.log('cursors', cursors) - - // this.editor.withoutSaving(() => { - // annotations.forEach(anno => { - // this.editor.removeAnnotation(anno) - // }) - - // Object.keys(cursors).forEach(key => { - // if (key !== this.socket.id && !keyMap[key]) { - // this.editor.addAnnotation(toJS(cursors[key])) - // } - // }) - // }) - - console.log( - '!!!!VAL', - this.connectOpts.query.name, - this.editor.value.toJSON({ preserveAnnotations: true }) - ) - } - receiveSlateOps = (operations: Immutable.List) => { const doc = this.docSet.getDoc(this.docId) const message = `change from ${this.socket.id}` @@ -130,26 +101,18 @@ class Connection { value: { selection } } = this.editor - const annotationType = 'collaborative_selection' - - const cursorData = { - id: this.socket.id, - selection, - // selectionOps: operations.filter(op => op.type === 'set_selection'), - annotationType - } - const withCursor = selection.isFocused ? setCursor : removeCursor const changed = Automerge.change(doc, message, (d: any) => withCursor( - applySlateOps(d, cursorOpFilter(operations, annotationType)), - cursorData + applySlateOps(d, cursorOpFilter(operations, this.cursorAnnotationType)), + this.socket.id, + selection, + this.cursorAnnotationType, + this.annotationDataMixin ) ) - console.log('doc with annotations!!', toJS(changed)) - this.docSet.setDoc(this.docId, changed) } @@ -174,7 +137,7 @@ class Connection { } connect = () => { - this.socket = io(this.url, this.connectOpts) + this.socket = io(this.url, { ...this.connectOpts }) this.socket.on('connect', () => { this.connection = new Automerge.Connection(this.docSet, this.sendData) @@ -190,6 +153,8 @@ class Connection { disconnect = () => { this.onDisconnect() + console.log('disconnect', this.socket) + this.connection && this.connection.close() delete this.connection @@ -202,6 +167,7 @@ class Connection { this.onDisconnect() this.socket.close() + // this.socket.destroy() } } diff --git a/packages/client/src/Controller.tsx b/packages/client/src/Controller.tsx index d5b4bbf..17c2e40 100644 --- a/packages/client/src/Controller.tsx +++ b/packages/client/src/Controller.tsx @@ -4,7 +4,6 @@ import { KeyUtils } from 'slate' import { hexGen } from '@slate-collaborative/bridge' import Connection from './Connection' - import { ControllerProps } from './model' class Controller extends Component { @@ -15,7 +14,13 @@ class Controller extends Component { } componentDidMount() { - const { editor, url, connectOpts } = this.props + const { + editor, + url, + cursorAnnotationType, + annotationDataMixin, + connectOpts + } = this.props KeyUtils.setGenerator(() => hexGen()) @@ -23,19 +28,11 @@ class Controller extends Component { editor, url, connectOpts, + cursorAnnotationType, + annotationDataMixin, onConnect: this.onConnect, onDisconnect: this.onDisconnect }) - - //@ts-ignore - if (!window.counter) { - //@ts-ignore - window.counter = 1 - } - //@ts-ignore - window[`Editor_${counter}`] = editor - //@ts-ignore - window.counter += 1 } componentWillUnmount() { @@ -47,10 +44,10 @@ class Controller extends Component { } render() { - const { children, preloader } = this.props + const { children, renderPreloader } = this.props const { preloading } = this.state - if (preloader && preloading) return preloader() + if (renderPreloader && preloading) return renderPreloader() return children } diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index 38f1b0d..112ea77 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -1,30 +1,34 @@ -import { ReactNode } from 'react' - import onChange from './onChange' import renderEditor from './renderEditor' import renderAnnotation from './renderAnnotation' -import Connection from './Connection' +import { PluginOptions } from './model' -export interface PluginOptions { - url?: string - connectOpts?: SocketIOClient.ConnectOpts - preloader?: () => ReactNode - onConnect?: (connection: Connection) => void - onDisconnect?: (connection: Connection) => void +export const defaultOpts = { + url: 'http://localhost:9000', + cursorAnnotationType: 'collaborative_selection', + annotationDataMixin: { + name: 'an collaborator' + }, + renderCursor: data => data.name, + cursorStyle: { + background: 'palevioletred' + }, + caretStyle: { + background: 'palevioletred' + }, + selectionStyle: { + background: 'rgba(233, 30, 99, 0.2)' + } } -const defaultOpts = { - url: 'http://localhost:9000' -} - -const plugin = (opts: PluginOptions = {}) => { +const plugin = (opts: PluginOptions = defaultOpts) => { const options = { ...defaultOpts, ...opts } return { onChange: onChange(options), renderEditor: renderEditor(options), - renderAnnotation + renderAnnotation: renderAnnotation(options) } } diff --git a/packages/client/src/model.ts b/packages/client/src/model.ts index 52b615c..7edf145 100644 --- a/packages/client/src/model.ts +++ b/packages/client/src/model.ts @@ -1,5 +1,7 @@ +import { ReactNode } from 'react' +import CSS from 'csstype' import { Editor, Controller, Value } from 'slate' -import { PluginOptions } from './index' + import Connection from './Connection' interface FixedController extends Controller { @@ -15,6 +17,7 @@ export interface ExtendedEditor extends Editor { export interface ConnectionModel extends PluginOptions { editor: ExtendedEditor + cursorAnnotationType: string onConnect: () => void onDisconnect: () => void } @@ -24,3 +27,18 @@ export interface ControllerProps extends PluginOptions { url?: string connectOpts?: SocketIOClient.ConnectOpts } + +export interface PluginOptions { + url?: string + connectOpts?: SocketIOClient.ConnectOpts + cursorAnnotationType?: string + caretStyle?: CSS.Properties + cursorStyle?: CSS.Properties + renderPreloader?: () => ReactNode + annotationDataMixin?: { + [key: string]: any + } + renderCursor?: (data: any) => ReactNode | string | any + onConnect?: (connection: Connection) => void + onDisconnect?: (connection: Connection) => void +} diff --git a/packages/client/src/renderAnnotation.tsx b/packages/client/src/renderAnnotation.tsx index 6c342c2..5db7aa0 100644 --- a/packages/client/src/renderAnnotation.tsx +++ b/packages/client/src/renderAnnotation.tsx @@ -1,7 +1,7 @@ import React, { Fragment } from 'react' const wrapStyles = { - backgroundColor: 'rgba(233, 30, 99, 0.2)', + background: 'rgba(233, 30, 99, 0.2)', position: 'relative' } @@ -24,50 +24,53 @@ const caretStyleBase = { userSelect: 'none', height: '100%', width: 2, - background: '#bf1b52' -} + background: 'palevioletred' +} as any -const renderAnnotation = (props, editor, next) => { +const renderAnnotation = ({ + cursorAnnotationType, + renderCursor, + cursorStyle = {}, + caretStyle = {}, + selectionStyle = {} +}) => (props, editor, next) => { const { children, annotation, attributes, node } = props + if (annotation.type !== cursorAnnotationType) return next() + const isBackward = annotation.data.get('isBackward') const targetPath = annotation.data.get('targetPath') + const cursorText = renderCursor(annotation.data) - console.log( - 'renderAnnotation', - annotation.toJS(), - props, - isBackward, - targetPath - ) - - const badgeStyles = { ...cursorStyleBase, left: isBackward ? '0%' : '100%' } - const caretStyles = { ...caretStyleBase, left: isBackward ? '0%' : '100%' } + const cursorStyles = { + ...cursorStyleBase, + ...cursorStyle, + left: isBackward ? '0%' : '100%' + } + const caretStyles = { + ...caretStyleBase, + ...caretStyle, + left: isBackward ? '0%' : '100%' + } const { document } = editor.value const targetNode = document.getNode(targetPath) - const isShowCursor = targetNode && targetNode.key === node.key - switch (annotation.type) { - case 'collaborative_selection': - return ( - - {isShowCursor ? ( - - - {annotation.key} - - - - ) : null} - {children} - - ) - default: - return next() - } + return ( + + {isShowCursor ? ( + + + {cursorText} + + + + ) : null} + {children} + + ) } export default renderAnnotation diff --git a/packages/client/src/renderEditor.tsx b/packages/client/src/renderEditor.tsx index 6a51db3..7355c6c 100644 --- a/packages/client/src/renderEditor.tsx +++ b/packages/client/src/renderEditor.tsx @@ -1,7 +1,6 @@ import React from 'react' -import { PluginOptions } from './index' - +import { PluginOptions } from './model' import Controller from './Controller' const renderEditor = (opts: PluginOptions) => ( diff --git a/packages/example/package.json b/packages/example/package.json index bacc6d7..60f837d 100644 --- a/packages/example/package.json +++ b/packages/example/package.json @@ -16,6 +16,7 @@ "concurrently": "^4.1.2", "faker": "^4.1.0", "lodash": "^4.17.15", + "randomcolor": "^0.5.4", "react": "^16.9.0", "react-dom": "^16.9.0", "react-scripts": "3.1.1", diff --git a/packages/example/src/Client.tsx b/packages/example/src/Client.tsx index 359ced2..6c40d9c 100644 --- a/packages/example/src/Client.tsx +++ b/packages/example/src/Client.tsx @@ -2,6 +2,7 @@ import React, { Component } from 'react' import { Value, ValueJSON } from 'slate' import { Editor } from 'slate-react' +import randomColor from 'randomcolor' import styled from '@emotion/styled' @@ -23,12 +24,18 @@ class Client extends Component { state = { value: Value.fromJSON(defaultValue as ValueJSON), - isOnline: true, + isOnline: false, plugins: [] } componentDidMount() { - const plugin = ClientPlugin({ + const color = randomColor({ + luminosity: 'dark', + format: 'rgba', + alpha: 1 + }) + + const options = { url: `http://localhost:9000/${this.props.slug}`, connectOpts: { query: { @@ -37,10 +44,25 @@ class Client extends Component { slug: this.props.slug } }, - // preloader: () =>
PRELOADER!!!!!!
, + annotationDataMixin: { + name: this.props.name + }, + cursorStyle: { + background: color + }, + caretStyle: { + background: color + }, + selectionStyle: { + background: color.slice(0, -2) + '0.2)' + }, + renderCursor: data => data.get('name'), + // renderPreloader: () =>
PRELOADER!!!!!!
, onConnect: this.onConnect, onDisconnect: this.onDisconnect - }) + } + + const plugin = ClientPlugin(options) this.setState({ plugins: [plugin] From 21a4a7d99a3a1b586f7f639c45bd6728d907932d Mon Sep 17 00:00:00 2001 From: cudr Date: Mon, 14 Oct 2019 00:49:28 +0300 Subject: [PATCH 7/7] feat: extract cursor component --- packages/backend/src/Connection.ts | 2 + packages/bridge/src/apply/annotation.ts | 2 +- packages/bridge/src/apply/index.ts | 5 +- packages/bridge/src/convert/index.ts | 4 +- packages/bridge/src/cursor/index.ts | 4 +- packages/client/Cursor.tsx | 0 packages/client/src/Cursor.tsx | 47 +++++++++++++ packages/client/src/{index.ts => index.tsx} | 17 ++--- packages/client/src/model.ts | 13 ++-- packages/client/src/renderAnnotation.tsx | 73 ++++----------------- packages/client/src/renderCursor.tsx | 7 ++ packages/example/package.json | 9 +-- packages/example/src/App.tsx | 12 +++- packages/example/src/Client.tsx | 14 +--- 14 files changed, 105 insertions(+), 104 deletions(-) delete mode 100644 packages/client/Cursor.tsx create mode 100644 packages/client/src/Cursor.tsx rename packages/client/src/{index.ts => index.tsx} (70%) create mode 100644 packages/client/src/renderCursor.tsx diff --git a/packages/backend/src/Connection.ts b/packages/backend/src/Connection.ts index aa27f4e..0940434 100644 --- a/packages/backend/src/Connection.ts +++ b/packages/backend/src/Connection.ts @@ -118,6 +118,8 @@ class Connection { this.connections[id].receiveMsg(data) this.saveDoc(name) + + this.garbageCursors(name) } catch (e) { console.log(e) } diff --git a/packages/bridge/src/apply/annotation.ts b/packages/bridge/src/apply/annotation.ts index e58bb85..dd7326a 100644 --- a/packages/bridge/src/apply/annotation.ts +++ b/packages/bridge/src/apply/annotation.ts @@ -1,4 +1,4 @@ -import { Operation, SyncDoc } from '../model' +import { Operation, SyncDoc } from '../model/index' export const addAnnotation = (doc: SyncDoc, op: Operation): SyncDoc => { console.log('addAnnotation!!!', op.toJS()) diff --git a/packages/bridge/src/apply/index.ts b/packages/bridge/src/apply/index.ts index c7d366c..2679830 100644 --- a/packages/bridge/src/apply/index.ts +++ b/packages/bridge/src/apply/index.ts @@ -21,7 +21,10 @@ export const applyOperation = (doc: SyncDoc, op: Operation): SyncDoc => { try { const applyOp = opType[op.type] - if (!applyOp) throw new TypeError('Unsupported operation type!') + if (!applyOp) { + console.log('operation', op.toJS()) + throw new TypeError(`Unsupported operation type: ${op.type}!`) + } return applyOp(doc, op) } catch (e) { diff --git a/packages/bridge/src/convert/index.ts b/packages/bridge/src/convert/index.ts index f0bc7a3..782e14c 100644 --- a/packages/bridge/src/convert/index.ts +++ b/packages/bridge/src/convert/index.ts @@ -30,9 +30,7 @@ const toSlateOp = (ops: Automerge.Diff[], doc) => { [] ]) - const res = defer.flatMap(op => op(tempTree, doc)).filter(op => op) - - return res + return defer.flatMap(op => op(tempTree, doc)).filter(op => op) } export { toSlateOp } diff --git a/packages/bridge/src/cursor/index.ts b/packages/bridge/src/cursor/index.ts index 480891e..c8d7087 100644 --- a/packages/bridge/src/cursor/index.ts +++ b/packages/bridge/src/cursor/index.ts @@ -1,5 +1,5 @@ import { Operation, Selection } from 'slate' -import { List } from 'immutable' +import * as Immutable from 'immutable' import merge from 'lodash/merge' import { toJS } from '../utils' @@ -51,7 +51,7 @@ export const removeCursor = (doc: SyncDoc, key: CursorKey) => { return doc } -export const cursorOpFilter = (ops: List, type: string) => +export const cursorOpFilter = (ops: Immutable.List, type: string) => ops.filter(op => { if (op.type === 'set_annotation') { return !( diff --git a/packages/client/Cursor.tsx b/packages/client/Cursor.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/packages/client/src/Cursor.tsx b/packages/client/src/Cursor.tsx new file mode 100644 index 0000000..3af7c45 --- /dev/null +++ b/packages/client/src/Cursor.tsx @@ -0,0 +1,47 @@ +import React, { Fragment } from 'react' + +const cursorStyleBase = { + position: 'absolute', + top: -2, + pointerEvents: 'none', + userSelect: 'none', + transform: 'translateY(-100%)', + fontSize: 10, + color: 'white', + background: 'palevioletred', + whiteSpace: 'nowrap' +} as any + +const caretStyleBase = { + position: 'absolute', + top: 0, + pointerEvents: 'none', + userSelect: 'none', + height: '100%', + width: 2, + background: 'palevioletred' +} as any + +const Cursor = ({ color, isBackward, name }) => { + const cursorStyles = { + ...cursorStyleBase, + background: color, + left: isBackward ? '0%' : '100%' + } + const caretStyles = { + ...caretStyleBase, + background: color, + left: isBackward ? '0%' : '100%' + } + + return ( + + + {name} + + + + ) +} + +export default Cursor diff --git a/packages/client/src/index.ts b/packages/client/src/index.tsx similarity index 70% rename from packages/client/src/index.ts rename to packages/client/src/index.tsx index 112ea77..0623a68 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.tsx @@ -2,23 +2,18 @@ import onChange from './onChange' import renderEditor from './renderEditor' import renderAnnotation from './renderAnnotation' +import renderCursor from './renderCursor' + import { PluginOptions } from './model' export const defaultOpts = { url: 'http://localhost:9000', cursorAnnotationType: 'collaborative_selection', + renderCursor, annotationDataMixin: { - name: 'an collaborator' - }, - renderCursor: data => data.name, - cursorStyle: { - background: 'palevioletred' - }, - caretStyle: { - background: 'palevioletred' - }, - selectionStyle: { - background: 'rgba(233, 30, 99, 0.2)' + name: 'an collaborator name', + color: 'palevioletred', + alphaColor: 'rgba(233, 30, 99, 0.2)' } } diff --git a/packages/client/src/model.ts b/packages/client/src/model.ts index 7edf145..ba98209 100644 --- a/packages/client/src/model.ts +++ b/packages/client/src/model.ts @@ -1,9 +1,12 @@ import { ReactNode } from 'react' -import CSS from 'csstype' import { Editor, Controller, Value } from 'slate' import Connection from './Connection' +type Data = { + [key: string]: any +} + interface FixedController extends Controller { setValue: (value: Value) => void } @@ -32,13 +35,9 @@ export interface PluginOptions { url?: string connectOpts?: SocketIOClient.ConnectOpts cursorAnnotationType?: string - caretStyle?: CSS.Properties - cursorStyle?: CSS.Properties + annotationDataMixin?: Data renderPreloader?: () => ReactNode - annotationDataMixin?: { - [key: string]: any - } - renderCursor?: (data: any) => ReactNode | string | any + renderCursor?: (data: Data) => ReactNode | any onConnect?: (connection: Connection) => void onDisconnect?: (connection: Connection) => void } diff --git a/packages/client/src/renderAnnotation.tsx b/packages/client/src/renderAnnotation.tsx index 5db7aa0..4114f0d 100644 --- a/packages/client/src/renderAnnotation.tsx +++ b/packages/client/src/renderAnnotation.tsx @@ -1,73 +1,28 @@ -import React, { Fragment } from 'react' +import React from 'react' -const wrapStyles = { - background: 'rgba(233, 30, 99, 0.2)', - position: 'relative' -} - -const cursorStyleBase = { - position: 'absolute', - top: -2, - pointerEvents: 'none', - userSelect: 'none', - transform: 'translateY(-100%)', - fontSize: 10, - color: 'white', - background: 'palevioletred', - whiteSpace: 'nowrap' -} as any - -const caretStyleBase = { - position: 'absolute', - top: 0, - pointerEvents: 'none', - userSelect: 'none', - height: '100%', - width: 2, - background: 'palevioletred' -} as any - -const renderAnnotation = ({ - cursorAnnotationType, - renderCursor, - cursorStyle = {}, - caretStyle = {}, - selectionStyle = {} -}) => (props, editor, next) => { +const renderAnnotation = ({ cursorAnnotationType, renderCursor }) => ( + props, + editor, + next +) => { const { children, annotation, attributes, node } = props if (annotation.type !== cursorAnnotationType) return next() - const isBackward = annotation.data.get('isBackward') - const targetPath = annotation.data.get('targetPath') - const cursorText = renderCursor(annotation.data) - - const cursorStyles = { - ...cursorStyleBase, - ...cursorStyle, - left: isBackward ? '0%' : '100%' - } - const caretStyles = { - ...caretStyleBase, - ...caretStyle, - left: isBackward ? '0%' : '100%' - } + const data = annotation.data.toJS() + const { targetPath, alphaColor } = data const { document } = editor.value const targetNode = document.getNode(targetPath) - const isShowCursor = targetNode && targetNode.key === node.key + const showCursor = targetNode && targetNode.key === node.key return ( - - {isShowCursor ? ( - - - {cursorText} - - - - ) : null} + + {showCursor ? renderCursor(data) : null} {children} ) diff --git a/packages/client/src/renderCursor.tsx b/packages/client/src/renderCursor.tsx new file mode 100644 index 0000000..d70c4f4 --- /dev/null +++ b/packages/client/src/renderCursor.tsx @@ -0,0 +1,7 @@ +import React from 'react' + +import Cursor from './Cursor' + +const renderCursor = data => + +export default renderCursor diff --git a/packages/example/package.json b/packages/example/package.json index 60f837d..5a05cd2 100644 --- a/packages/example/package.json +++ b/packages/example/package.json @@ -16,6 +16,7 @@ "concurrently": "^4.1.2", "faker": "^4.1.0", "lodash": "^4.17.15", + "nodemon": "^1.19.2", "randomcolor": "^0.5.4", "react": "^16.9.0", "react-dom": "^16.9.0", @@ -26,10 +27,9 @@ }, "scripts": { "start": "react-scripts start", + "build": "react-scripts build", "dev": "concurrently \"yarn start\" \"yarn serve\"", - "serve": "nodemon --watch ../backend/lib --inspect server.js", - "test": "react-scripts test", - "eject": "react-scripts eject" + "serve": "nodemon --watch ../backend/lib --inspect server.js" }, "eslintConfig": { "extends": "react-app" @@ -45,8 +45,5 @@ "last 1 firefox version", "last 1 safari version" ] - }, - "devDependencies": { - "nodemon": "^1.19.2" } } diff --git a/packages/example/src/App.tsx b/packages/example/src/App.tsx index c1fbd9f..64d00e1 100644 --- a/packages/example/src/App.tsx +++ b/packages/example/src/App.tsx @@ -19,9 +19,11 @@ class App extends Component<{}, { rooms: string[] }> { return ( - - Add Room - + + + Add Room + + {rooms.map(room => ( ))} @@ -46,6 +48,10 @@ export default App const Container = styled.div`` +const Panel = styled.div` + display: flex; +` + const Button = styled.button` padding: 6px 14px; display: block; diff --git a/packages/example/src/Client.tsx b/packages/example/src/Client.tsx index 6c40d9c..21eaea2 100644 --- a/packages/example/src/Client.tsx +++ b/packages/example/src/Client.tsx @@ -45,18 +45,10 @@ class Client extends Component { } }, annotationDataMixin: { - name: this.props.name + name: this.props.name, + color, + alphaColor: color.slice(0, -2) + '0.2)' }, - cursorStyle: { - background: color - }, - caretStyle: { - background: color - }, - selectionStyle: { - background: color.slice(0, -2) + '0.2)' - }, - renderCursor: data => data.get('name'), // renderPreloader: () =>
PRELOADER!!!!!!
, onConnect: this.onConnect, onDisconnect: this.onDisconnect