From 0ceb38bbfd26d90e2b712ea450badbdb8b5c7b52 Mon Sep 17 00:00:00 2001 From: cudr Date: Sun, 13 Oct 2019 19:37:56 +0300 Subject: [PATCH] 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]