mirror of
https://github.com/cudr/slate-collaborative.git
synced 2026-03-02 03:40:18 +00:00
initial commit
This commit is contained in:
7
packages/client/.babelrc
Normal file
7
packages/client/.babelrc
Normal 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
1
packages/client/.npmrc
Normal file
@@ -0,0 +1 @@
|
||||
scripts-prepend-node-path=true
|
||||
41
packages/client/package.json
Normal file
41
packages/client/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
141
packages/client/src/Connection.ts
Normal file
141
packages/client/src/Connection.ts
Normal 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
|
||||
64
packages/client/src/Controller.tsx
Normal file
64
packages/client/src/Controller.tsx
Normal 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
|
||||
29
packages/client/src/index.ts
Normal file
29
packages/client/src/index.ts
Normal 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
|
||||
20
packages/client/src/model.ts
Normal file
20
packages/client/src/model.ts
Normal 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
|
||||
}
|
||||
11
packages/client/src/onChange.ts
Normal file
11
packages/client/src/onChange.ts
Normal 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
|
||||
21
packages/client/src/renderEditor.tsx
Normal file
21
packages/client/src/renderEditor.tsx
Normal 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
|
||||
12
packages/client/tsconfig.json
Normal file
12
packages/client/tsconfig.json
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user