initial commit

This commit is contained in:
cudr
2019-10-05 11:44:49 +03:00
commit a817eb1ceb
63 changed files with 1769 additions and 0 deletions

7
packages/client/.babelrc Normal file
View File

@@ -0,0 +1,7 @@
{
"presets": ["@babel/env", "@babel/react", "@babel/typescript"],
"plugins": [
"@babel/proposal-class-properties",
"@babel/proposal-object-rest-spread"
]
}

1
packages/client/.npmrc Normal file
View File

@@ -0,0 +1 @@
scripts-prepend-node-path=true

View File

@@ -0,0 +1,41 @@
{
"name": "@slate-collaborative/client",
"version": "0.0.1",
"files": [
"lib"
],
"main": "lib/index.js",
"types": "lib/index.d.ts",
"description": "slate-collaborative bridge",
"repository": "https://github.com/cudr/slate-collaborative",
"author": "cudr",
"license": "MIT",
"scripts": {
"prepublishOnly": "npm run build",
"build": "npm run build:types && npm run build:js",
"build:types": "tsc --emitDeclarationOnly",
"build:js": "babel src --out-dir lib --extensions \".ts,.tsx\" --source-maps inline",
"watch": "yarn build:js -w"
},
"dependencies": {
"@babel/preset-react": "^7.0.0",
"@slate-collaborative/bridge": "^0.0.1",
"automerge": "^0.12.1",
"immutable": "^4.0.0-rc.12",
"react": "^16.9.0",
"slate": "^0.47.8",
"socket.io-client": "^2.2.0",
"typescript": "^3.6.3"
},
"devDependencies": {
"@babel/cli": "^7.6.0",
"@babel/core": "^7.6.0",
"@babel/plugin-proposal-class-properties": "^7.5.5",
"@babel/plugin-proposal-object-rest-spread": "^7.5.5",
"@babel/preset-env": "^7.6.0",
"@babel/preset-typescript": "^7.6.0",
"@types/react": "^16.9.2",
"@types/slate": "^0.47.1",
"@types/socket.io-client": "^1.4.32"
}
}

View File

@@ -0,0 +1,141 @@
import Automerge from 'automerge'
import Immutable from 'immutable'
import io from 'socket.io-client'
import { Value, Operation } from 'slate'
import { ConnectionModel } from './model'
import { applySlateOps, toSlateOp, toJS } from '@slate-collaborative/bridge'
class Connection {
url: string
docId: string
docSet: Automerge.DocSet<any>
connection: Automerge.Connection<any>
socket: SocketIOClient.Socket
editor: any
connectOpts: any
onConnect?: () => void
onDisconnect?: () => void
constructor({
editor,
url,
connectOpts,
onConnect,
onDisconnect
}: ConnectionModel) {
this.url = url
this.editor = editor
this.connectOpts = connectOpts
this.onConnect = onConnect
this.onDisconnect = onDisconnect
this.docId = connectOpts.path || new URL(url).pathname
this.docSet = new Automerge.DocSet()
this.connect()
}
sendData = (data: any) => {
this.socket.emit('operation', data)
}
recieveData = async (data: any) => {
if (this.docId !== data.docId || !this.connection) {
return
}
const currentDoc = this.docSet.getDoc(this.docId)
const docNew = this.connection.receiveMsg(data)
if (!docNew) {
return
}
try {
const operations = Automerge.diff(currentDoc, docNew)
if (operations.length !== 0) {
const slateOps = toSlateOp(operations, this.connectOpts.query.name)
this.editor.remote = true
this.editor.withoutSaving(() => {
slateOps.forEach(o => this.editor.applyOperation(o))
})
setTimeout(() => (this.editor.remote = false), 5)
}
} catch (e) {
console.error(e)
}
}
receiveSlateOps = (operations: Immutable.List<Operation>) => {
const doc = this.docSet.getDoc(this.docId)
const message = `change from ${this.socket.id}`
if (!doc) return
const changed = Automerge.change(doc, message, (d: any) =>
applySlateOps(d, operations)
)
this.docSet.setDoc(this.docId, changed)
}
recieveDocument = data => {
const currentDoc = this.docSet.getDoc(this.docId)
if (!currentDoc) {
const doc = Automerge.load(data)
this.docSet.removeDoc(this.docId)
this.docSet.setDoc(this.docId, doc)
this.editor.controller.setValue(Value.fromJSON(toJS(doc)))
}
this.editor.setFocus()
this.connection.open()
this.onConnect && setTimeout(this.onConnect, 0)
}
connect = () => {
this.socket = io(this.url, this.connectOpts)
this.socket.on('connect', () => {
this.connection = new Automerge.Connection(this.docSet, this.sendData)
this.socket.on('document', this.recieveDocument)
this.socket.on('operation', this.recieveData)
this.socket.on('disconnect', this.disconnect)
})
}
disconnect = () => {
this.onDisconnect()
this.connection && this.connection.close()
delete this.connection
this.socket.removeListener('document')
this.socket.removeListener('operation')
}
close = () => {
this.onDisconnect()
this.socket.close()
}
}
export default Connection

View File

@@ -0,0 +1,64 @@
import React, { PureComponent, ReactNode } from 'react'
import Connection from './Connection'
import { ControllerProps } from './model'
class Controller extends PureComponent<ControllerProps> {
connection?: Connection
state = {
preloading: true
}
componentDidMount() {
const { editor, url, connectOpts } = this.props
editor.connection = new Connection({
editor,
url,
connectOpts,
onConnect: this.onConnect,
onDisconnect: this.onDisconnect
})
}
componentWillUnmount() {
const { editor } = this.props
if (editor.connection) editor.connection.close()
delete editor.connection
}
render() {
const { children, preloader } = this.props
const { preloading } = this.state
if (preloader && preloading) return preloader()
return children
}
onConnect = () => {
const { onConnect, editor } = this.props
this.setState({
preloading: false
})
onConnect && onConnect(editor.connection)
}
onDisconnect = () => {
const { onDisconnect, editor } = this.props
this.setState({
preloading: true
})
onDisconnect && onDisconnect(editor.connection)
}
}
export default Controller

View File

@@ -0,0 +1,29 @@
import { ReactNode } from 'react'
import onChange from './onChange'
import renderEditor from './renderEditor'
import Connection from './Connection'
export interface PluginOptions {
url?: string
connectOpts?: SocketIOClient.ConnectOpts
preloader?: () => ReactNode
onConnect?: (connection: Connection) => void
onDisconnect?: (connection: Connection) => void
}
const defaultOpts = {
url: 'http://localhost:9000'
}
const plugin = (opts: PluginOptions = {}) => {
const options = { ...defaultOpts, ...opts }
return {
onChange: onChange(options),
renderEditor: renderEditor(options)
}
}
export default plugin

View File

@@ -0,0 +1,20 @@
import { Editor } from 'slate'
import { PluginOptions } from './index'
import Connection from './Connection'
export interface ConnectionModel extends PluginOptions {
editor: Editor
onConnect: () => void
onDisconnect: () => void
}
export interface ExtendedEditor extends Editor {
remote: boolean
connection: Connection
}
export interface ControllerProps extends PluginOptions {
editor: ExtendedEditor
url?: string
connectOpts?: SocketIOClient.ConnectOpts
}

View File

@@ -0,0 +1,11 @@
import { ExtendedEditor } from './model'
const onChange = opts => (editor: ExtendedEditor, next: () => void) => {
if (!editor.remote) {
editor.connection.receiveSlateOps(editor.operations)
}
return next()
}
export default onChange

View File

@@ -0,0 +1,21 @@
import React from 'react'
import { PluginOptions } from './index'
import Controller from './Controller'
const renderEditor = (opts: PluginOptions) => (
props: any,
editor: any,
next: any
) => {
const children = next()
return (
<Controller {...opts} editor={editor}>
{children}
</Controller>
)
}
export default renderEditor

View File

@@ -0,0 +1,12 @@
{
"include": ["src/**/*"],
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "lib",
"rootDir": "src",
"baseUrl": "src",
"jsx": "react",
"lib": ["es6", "dom"],
"esModuleInterop": true
}
}