mirror of
https://github.com/cudr/slate-collaborative.git
synced 2026-03-02 03:40:18 +00:00
feat: update to slate 0.5x (#10)
Update Slate-Collaboration to be compatible with Slate 0.5x versions.
This commit is contained in:
@@ -1,206 +0,0 @@
|
||||
import Automerge from 'automerge'
|
||||
import Immutable from 'immutable'
|
||||
import io from 'socket.io-client'
|
||||
|
||||
import { Value, Operation } from 'slate'
|
||||
import { ConnectionModel, ExtendedEditor } from './model'
|
||||
|
||||
import {
|
||||
setCursor,
|
||||
removeCursor,
|
||||
cursorOpFilter,
|
||||
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: ExtendedEditor
|
||||
connectOpts: any
|
||||
annotationDataMixin: any
|
||||
cursorAnnotationType: string
|
||||
onConnect?: () => void
|
||||
onDisconnect?: () => void
|
||||
|
||||
constructor({
|
||||
editor,
|
||||
url,
|
||||
connectOpts,
|
||||
onConnect,
|
||||
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
|
||||
|
||||
this.docId = connectOpts.path || new URL(url).pathname
|
||||
|
||||
this.docSet = new Automerge.DocSet()
|
||||
|
||||
this.connect()
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
sendData = (data: any) => {
|
||||
this.socket.emit('operation', data)
|
||||
}
|
||||
|
||||
recieveData = async (data: any) => {
|
||||
if (this.docId !== data.docId || !this.connection) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const currentDoc = this.docSet.getDoc(this.docId)
|
||||
const docNew = this.connection.receiveMsg(data)
|
||||
|
||||
if (!docNew) {
|
||||
return
|
||||
}
|
||||
|
||||
const operations = Automerge.diff(currentDoc, docNew)
|
||||
|
||||
if (operations.length !== 0) {
|
||||
const slateOps = toSlateOp(operations, currentDoc)
|
||||
|
||||
this.editor.remote = true
|
||||
|
||||
this.editor.withoutSaving(() => {
|
||||
slateOps.forEach(o => {
|
||||
this.editor.applyOperation(o)
|
||||
})
|
||||
})
|
||||
|
||||
await Promise.resolve()
|
||||
|
||||
this.editor.remote = false
|
||||
|
||||
this.garbageCursors()
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
garbageCursors = async () => {
|
||||
const doc = this.docSet.getDoc(this.docId)
|
||||
const { value } = this.editor
|
||||
|
||||
if (value.annotations.size === Object.keys(doc.annotations).length) {
|
||||
return
|
||||
}
|
||||
|
||||
const garbage = []
|
||||
|
||||
value.annotations.forEach(annotation => {
|
||||
if (
|
||||
annotation.type === this.cursorAnnotationType &&
|
||||
!doc.annotations[annotation.key]
|
||||
) {
|
||||
garbage.push(annotation)
|
||||
}
|
||||
})
|
||||
|
||||
if (garbage.length) {
|
||||
this.editor.withoutSaving(() => {
|
||||
garbage.forEach(annotation => {
|
||||
this.editor.removeAnnotation(annotation)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
receiveSlateOps = (operations: Immutable.List<Operation>) => {
|
||||
const doc = this.docSet.getDoc(this.docId)
|
||||
const message = `change from ${this.socket.id}`
|
||||
|
||||
if (!doc) return
|
||||
|
||||
const {
|
||||
value: { selection }
|
||||
} = this.editor
|
||||
|
||||
const withCursor = selection.isFocused ? setCursor : removeCursor
|
||||
|
||||
const changed = Automerge.change(doc, message, (d: any) =>
|
||||
withCursor(
|
||||
applySlateOps(d, cursorOpFilter(operations, this.cursorAnnotationType)),
|
||||
this.socket.id,
|
||||
selection,
|
||||
this.cursorAnnotationType,
|
||||
this.annotationDataMixin
|
||||
)
|
||||
)
|
||||
|
||||
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()
|
||||
|
||||
console.log('disconnect', this.socket)
|
||||
|
||||
this.connection && this.connection.close()
|
||||
|
||||
delete this.connection
|
||||
|
||||
this.socket.removeListener('document')
|
||||
this.socket.removeListener('operation')
|
||||
}
|
||||
|
||||
close = () => {
|
||||
this.onDisconnect()
|
||||
|
||||
this.socket.close()
|
||||
// this.socket.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
export default Connection
|
||||
@@ -1,76 +0,0 @@
|
||||
import React, { Component } from 'react'
|
||||
import { KeyUtils } from 'slate'
|
||||
|
||||
import { hexGen } from '@slate-collaborative/bridge'
|
||||
|
||||
import Connection from './Connection'
|
||||
import { ControllerProps } from './model'
|
||||
|
||||
class Controller extends Component<ControllerProps> {
|
||||
connection?: Connection
|
||||
|
||||
state = {
|
||||
preloading: true
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
editor,
|
||||
url,
|
||||
cursorAnnotationType,
|
||||
annotationDataMixin,
|
||||
connectOpts
|
||||
} = this.props
|
||||
|
||||
KeyUtils.setGenerator(() => hexGen())
|
||||
|
||||
editor.connection = new Connection({
|
||||
editor,
|
||||
url,
|
||||
connectOpts,
|
||||
cursorAnnotationType,
|
||||
annotationDataMixin,
|
||||
onConnect: this.onConnect,
|
||||
onDisconnect: this.onDisconnect
|
||||
})
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const { editor } = this.props
|
||||
|
||||
if (editor.connection) editor.connection.close()
|
||||
|
||||
delete editor.connection
|
||||
}
|
||||
|
||||
render() {
|
||||
const { children, renderPreloader } = this.props
|
||||
const { preloading } = this.state
|
||||
|
||||
if (renderPreloader && preloading) return renderPreloader()
|
||||
|
||||
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
|
||||
@@ -1,47 +0,0 @@
|
||||
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 (
|
||||
<Fragment>
|
||||
<span contentEditable={false} style={cursorStyles}>
|
||||
{name}
|
||||
</span>
|
||||
<span contentEditable={false} style={caretStyles} />
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
export default Cursor
|
||||
156
packages/client/src/automerge-editor.ts
Normal file
156
packages/client/src/automerge-editor.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import Automerge from 'automerge'
|
||||
|
||||
import { Editor, Operation } from 'slate'
|
||||
|
||||
import {
|
||||
toJS,
|
||||
SyncDoc,
|
||||
CollabAction,
|
||||
toCollabAction,
|
||||
applyOperation,
|
||||
setCursor,
|
||||
toSlateOp,
|
||||
CursorData
|
||||
} from '@slate-collaborative/bridge'
|
||||
|
||||
export interface AutomergeEditor extends Editor {
|
||||
clientId: string
|
||||
|
||||
isRemote: boolean
|
||||
|
||||
docSet: Automerge.DocSet<SyncDoc>
|
||||
connection: Automerge.Connection<SyncDoc>
|
||||
|
||||
onConnectionMsg: (msg: Automerge.Message) => void
|
||||
|
||||
openConnection: () => void
|
||||
closeConnection: () => void
|
||||
|
||||
receiveDocument: (data: string) => void
|
||||
receiveOperation: (data: Automerge.Message) => void
|
||||
|
||||
gabageCursor: () => void
|
||||
|
||||
onCursor: (data: any) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* `AutomergeEditor` contains methods for collaboration-enabled editors.
|
||||
*/
|
||||
|
||||
export const AutomergeEditor = {
|
||||
/**
|
||||
* Create Automerge connection
|
||||
*/
|
||||
|
||||
createConnection: (e: AutomergeEditor, emit: (data: CollabAction) => void) =>
|
||||
new Automerge.Connection(e.docSet, toCollabAction('operation', emit)),
|
||||
|
||||
/**
|
||||
* Apply Slate operations to Automerge
|
||||
*/
|
||||
|
||||
applySlateOps: async (
|
||||
e: AutomergeEditor,
|
||||
docId: string,
|
||||
operations: Operation[],
|
||||
cursorData?: CursorData
|
||||
) => {
|
||||
try {
|
||||
const doc = e.docSet.getDoc(docId)
|
||||
|
||||
if (!doc) {
|
||||
throw new TypeError(`Unknown docId: ${docId}!`)
|
||||
}
|
||||
|
||||
let changed
|
||||
|
||||
for await (let op of operations) {
|
||||
changed = Automerge.change(changed || doc, d =>
|
||||
applyOperation(d.children, op)
|
||||
)
|
||||
}
|
||||
|
||||
changed = Automerge.change(changed || doc, d => {
|
||||
setCursor(e.clientId, e.selection, d, operations, cursorData || {})
|
||||
})
|
||||
|
||||
e.docSet.setDoc(docId, changed as any)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Receive and apply document to Automerge docSet
|
||||
*/
|
||||
|
||||
receiveDocument: (e: AutomergeEditor, docId: string, data: string) => {
|
||||
const currentDoc = e.docSet.getDoc(docId)
|
||||
|
||||
const externalDoc = Automerge.load<SyncDoc>(data)
|
||||
|
||||
const mergedDoc = Automerge.merge<SyncDoc>(
|
||||
externalDoc,
|
||||
currentDoc || Automerge.init()
|
||||
)
|
||||
|
||||
e.docSet.setDoc(docId, mergedDoc)
|
||||
|
||||
Editor.withoutNormalizing(e, () => {
|
||||
e.children = toJS(mergedDoc).children
|
||||
|
||||
e.onChange()
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Generate automerge diff, convert and apply operations to Editor
|
||||
*/
|
||||
|
||||
applyOperation: (
|
||||
e: AutomergeEditor,
|
||||
docId: string,
|
||||
data: Automerge.Message
|
||||
) => {
|
||||
try {
|
||||
const current: any = e.docSet.getDoc(docId)
|
||||
|
||||
const updated = e.connection.receiveMsg(data)
|
||||
|
||||
const operations = Automerge.diff(current, updated)
|
||||
|
||||
if (operations.length) {
|
||||
const slateOps = toSlateOp(operations, current)
|
||||
|
||||
e.isRemote = true
|
||||
|
||||
Editor.withoutNormalizing(e, () => {
|
||||
slateOps.forEach((o: Operation) => {
|
||||
e.apply(o)
|
||||
})
|
||||
})
|
||||
|
||||
e.onCursor && e.onCursor(updated.cursors)
|
||||
|
||||
Promise.resolve().then(_ => (e.isRemote = false))
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
},
|
||||
|
||||
garbageCursor: (e: AutomergeEditor, docId: string) => {
|
||||
const doc = e.docSet.getDoc(docId)
|
||||
|
||||
const changed = Automerge.change<SyncDoc>(doc, d => {
|
||||
delete d.cusors
|
||||
})
|
||||
|
||||
e.onCursor && e.onCursor(null)
|
||||
|
||||
e.docSet.setDoc(docId, changed)
|
||||
|
||||
e.onChange()
|
||||
}
|
||||
}
|
||||
6
packages/client/src/index.ts
Normal file
6
packages/client/src/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import useCursor from './useCursor'
|
||||
import withAutomerge from './withAutomerge'
|
||||
import withSocketIO from './withSocketIO'
|
||||
import withIOCollaboration from './withIOCollaboration'
|
||||
|
||||
export { withAutomerge, withSocketIO, withIOCollaboration, useCursor }
|
||||
@@ -1,30 +0,0 @@
|
||||
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 name',
|
||||
color: 'palevioletred',
|
||||
alphaColor: 'rgba(233, 30, 99, 0.2)'
|
||||
}
|
||||
}
|
||||
|
||||
const plugin = (opts: PluginOptions = defaultOpts) => {
|
||||
const options = { ...defaultOpts, ...opts }
|
||||
|
||||
return {
|
||||
onChange: onChange(options),
|
||||
renderEditor: renderEditor(options),
|
||||
renderAnnotation: renderAnnotation(options)
|
||||
}
|
||||
}
|
||||
|
||||
export default plugin
|
||||
@@ -1,43 +0,0 @@
|
||||
import { ReactNode } from 'react'
|
||||
import { Editor, Controller, Value } from 'slate'
|
||||
|
||||
import Connection from './Connection'
|
||||
|
||||
type Data = {
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
interface FixedController extends Controller {
|
||||
setValue: (value: Value) => void
|
||||
}
|
||||
|
||||
export interface ExtendedEditor extends Editor {
|
||||
remote?: boolean
|
||||
connection?: Connection
|
||||
controller: FixedController
|
||||
setFocus: () => void
|
||||
}
|
||||
|
||||
export interface ConnectionModel extends PluginOptions {
|
||||
editor: ExtendedEditor
|
||||
cursorAnnotationType: string
|
||||
onConnect: () => void
|
||||
onDisconnect: () => void
|
||||
}
|
||||
|
||||
export interface ControllerProps extends PluginOptions {
|
||||
editor: ExtendedEditor
|
||||
url?: string
|
||||
connectOpts?: SocketIOClient.ConnectOpts
|
||||
}
|
||||
|
||||
export interface PluginOptions {
|
||||
url?: string
|
||||
connectOpts?: SocketIOClient.ConnectOpts
|
||||
cursorAnnotationType?: string
|
||||
annotationDataMixin?: Data
|
||||
renderPreloader?: () => ReactNode
|
||||
renderCursor?: (data: Data) => ReactNode | any
|
||||
onConnect?: (connection: Connection) => void
|
||||
onDisconnect?: (connection: Connection) => void
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { ExtendedEditor } from './model'
|
||||
|
||||
const onChange = opts => (editor: ExtendedEditor, next: () => void) => {
|
||||
if (editor.connection && !editor.remote) {
|
||||
const operations: any = editor.operations
|
||||
|
||||
editor.connection.receiveSlateOps(operations)
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
|
||||
export default onChange
|
||||
@@ -1,31 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
const renderAnnotation = ({ cursorAnnotationType, renderCursor }) => (
|
||||
props,
|
||||
editor,
|
||||
next
|
||||
) => {
|
||||
const { children, annotation, attributes, node } = props
|
||||
|
||||
if (annotation.type !== cursorAnnotationType) return next()
|
||||
|
||||
const data = annotation.data.toJS()
|
||||
|
||||
const { targetPath, alphaColor } = data
|
||||
const { document } = editor.value
|
||||
|
||||
const targetNode = document.getNode(targetPath)
|
||||
const showCursor = targetNode && targetNode.key === node.key
|
||||
|
||||
return (
|
||||
<span
|
||||
{...attributes}
|
||||
style={{ position: 'relative', background: alphaColor }}
|
||||
>
|
||||
{showCursor ? renderCursor(data) : null}
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export default renderAnnotation
|
||||
@@ -1,7 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
import Cursor from './Cursor'
|
||||
|
||||
const renderCursor = data => <Cursor {...data} />
|
||||
|
||||
export default renderCursor
|
||||
@@ -1,20 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
import { PluginOptions } from './model'
|
||||
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
|
||||
68
packages/client/src/useCursor.ts
Normal file
68
packages/client/src/useCursor.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { useState, useCallback, useEffect, useMemo } from 'react'
|
||||
|
||||
import { Text, Range, Path, NodeEntry } from 'slate'
|
||||
|
||||
import { toJS, Cursor, Cursors } from '@slate-collaborative/bridge'
|
||||
|
||||
import { AutomergeEditor } from './automerge-editor'
|
||||
|
||||
const useCursor = (
|
||||
e: AutomergeEditor
|
||||
): { decorate: (entry: NodeEntry) => Range[]; cursors: Cursor[] } => {
|
||||
const [cursorData, setSursorData] = useState<Cursor[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
e.onCursor = (data: Cursors) => {
|
||||
const ranges: Cursor[] = []
|
||||
|
||||
const cursors = toJS(data)
|
||||
|
||||
for (let cursor in cursors) {
|
||||
if (cursor !== e.clientId && cursors[cursor]) {
|
||||
ranges.push(JSON.parse(cursors[cursor]))
|
||||
}
|
||||
}
|
||||
|
||||
setSursorData(ranges)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const cursors = useMemo<Cursor[]>(() => cursorData, [cursorData])
|
||||
|
||||
const decorate = useCallback(
|
||||
([node, path]: NodeEntry) => {
|
||||
const ranges: Range[] = []
|
||||
|
||||
if (Text.isText(node) && cursors?.length) {
|
||||
cursors.forEach(cursor => {
|
||||
if (Range.includes(cursor, path)) {
|
||||
const { focus, anchor, isForward } = cursor
|
||||
|
||||
ranges.push({
|
||||
...cursor,
|
||||
isCaret: isForward
|
||||
? Path.equals(focus.path, path)
|
||||
: Path.equals(anchor.path, path),
|
||||
anchor: Path.isBefore(anchor.path, path)
|
||||
? { ...anchor, offset: 0 }
|
||||
: anchor,
|
||||
focus: Path.isAfter(focus.path, path)
|
||||
? { ...focus, offset: node.text.length }
|
||||
: focus
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return ranges
|
||||
},
|
||||
[cursors]
|
||||
)
|
||||
|
||||
return {
|
||||
cursors,
|
||||
decorate
|
||||
}
|
||||
}
|
||||
|
||||
export default useCursor
|
||||
105
packages/client/src/withAutomerge.ts
Normal file
105
packages/client/src/withAutomerge.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import Automerge from 'automerge'
|
||||
|
||||
import { Editor } from 'slate'
|
||||
|
||||
import { AutomergeEditor } from './automerge-editor'
|
||||
|
||||
import { CursorData, CollabAction } from '@slate-collaborative/bridge'
|
||||
|
||||
export interface AutomergeOptions {
|
||||
docId: string
|
||||
cursorData?: CursorData
|
||||
}
|
||||
|
||||
/**
|
||||
* The `withAutomerge` plugin contains core collaboration logic.
|
||||
*/
|
||||
|
||||
const withAutomerge = <T extends Editor>(
|
||||
editor: T,
|
||||
options: AutomergeOptions
|
||||
) => {
|
||||
const e = editor as T & AutomergeEditor
|
||||
|
||||
const { onChange } = e
|
||||
|
||||
const { docId, cursorData } = options || {}
|
||||
|
||||
e.docSet = new Automerge.DocSet()
|
||||
|
||||
const createConnection = () => {
|
||||
if (e.connection) e.connection.close()
|
||||
|
||||
e.connection = AutomergeEditor.createConnection(e, (data: CollabAction) =>
|
||||
e.send(data)
|
||||
)
|
||||
|
||||
e.connection.open()
|
||||
}
|
||||
|
||||
createConnection()
|
||||
|
||||
/**
|
||||
* Open Automerge Connection
|
||||
*/
|
||||
|
||||
e.openConnection = () => {
|
||||
e.connection.open()
|
||||
}
|
||||
|
||||
/**
|
||||
* Close Automerge Connection
|
||||
*/
|
||||
|
||||
e.closeConnection = () => {
|
||||
e.connection.close()
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cursor data
|
||||
*/
|
||||
|
||||
e.gabageCursor = () => {
|
||||
AutomergeEditor.garbageCursor(e, docId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Editor onChange
|
||||
*/
|
||||
|
||||
e.onChange = () => {
|
||||
const operations: any = e.operations
|
||||
|
||||
if (!e.isRemote) {
|
||||
AutomergeEditor.applySlateOps(e, docId, operations, cursorData)
|
||||
}
|
||||
|
||||
onChange()
|
||||
|
||||
// console.log('e', e.children)
|
||||
}
|
||||
|
||||
/**
|
||||
* Receive document value
|
||||
*/
|
||||
|
||||
e.receiveDocument = data => {
|
||||
AutomergeEditor.receiveDocument(e, docId, data)
|
||||
|
||||
createConnection()
|
||||
}
|
||||
|
||||
/**
|
||||
* Receive Automerge sync operations
|
||||
*/
|
||||
|
||||
e.receiveOperation = data => {
|
||||
if (docId !== data.docId) return
|
||||
|
||||
AutomergeEditor.applyOperation(e, docId, data)
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
export default withAutomerge
|
||||
20
packages/client/src/withIOCollaboration.ts
Normal file
20
packages/client/src/withIOCollaboration.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Editor } from 'slate'
|
||||
import { AutomergeEditor } from './automerge-editor'
|
||||
|
||||
import withAutomerge, { AutomergeOptions } from './withAutomerge'
|
||||
import withSocketIO, {
|
||||
WithSocketIOEditor,
|
||||
SocketIOPluginOptions
|
||||
} from './withSocketIO'
|
||||
|
||||
/**
|
||||
* The `withIOCollaboration` plugin contains collaboration with SocketIO.
|
||||
*/
|
||||
|
||||
const withIOCollaboration = <T extends Editor>(
|
||||
editor: T,
|
||||
options: AutomergeOptions & SocketIOPluginOptions
|
||||
): T & WithSocketIOEditor & AutomergeEditor =>
|
||||
withSocketIO(withAutomerge(editor, options), options)
|
||||
|
||||
export default withIOCollaboration
|
||||
122
packages/client/src/withSocketIO.ts
Normal file
122
packages/client/src/withSocketIO.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import io from 'socket.io-client'
|
||||
|
||||
import { AutomergeEditor } from './automerge-editor'
|
||||
|
||||
import { CollabAction } from '@slate-collaborative/bridge'
|
||||
|
||||
export interface SocketIOPluginOptions {
|
||||
url: string
|
||||
connectOpts: SocketIOClient.ConnectOpts
|
||||
autoConnect?: boolean
|
||||
|
||||
onConnect?: () => void
|
||||
onDisconnect?: () => void
|
||||
}
|
||||
|
||||
export interface WithSocketIOEditor {
|
||||
socket: SocketIOClient.Socket
|
||||
|
||||
connect: () => void
|
||||
disconnect: () => void
|
||||
|
||||
send: (op: CollabAction) => void
|
||||
receive: (op: CollabAction) => void
|
||||
|
||||
destroy: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* The `withSocketIO` plugin contains SocketIO layer logic.
|
||||
*/
|
||||
|
||||
const withSocketIO = <T extends AutomergeEditor>(
|
||||
editor: T,
|
||||
options: SocketIOPluginOptions
|
||||
) => {
|
||||
const e = editor as T & WithSocketIOEditor
|
||||
|
||||
const { onConnect, onDisconnect, connectOpts, url, autoConnect } = options
|
||||
|
||||
/**
|
||||
* Connect to Socket.
|
||||
*/
|
||||
|
||||
e.connect = () => {
|
||||
if (!e.socket) {
|
||||
e.socket = io(url, { ...connectOpts })
|
||||
|
||||
e.socket.on('connect', () => {
|
||||
e.clientId = e.socket.id
|
||||
|
||||
e.openConnection()
|
||||
|
||||
onConnect && onConnect()
|
||||
})
|
||||
}
|
||||
|
||||
e.socket.on('msg', (data: CollabAction) => {
|
||||
e.receive(data)
|
||||
})
|
||||
|
||||
e.socket.on('disconnect', () => {
|
||||
e.gabageCursor()
|
||||
|
||||
onDisconnect && onDisconnect()
|
||||
})
|
||||
|
||||
e.socket.connect()
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from Socket.
|
||||
*/
|
||||
|
||||
e.disconnect = () => {
|
||||
e.socket.removeListener('msg')
|
||||
|
||||
e.socket.close()
|
||||
|
||||
e.closeConnection()
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
/**
|
||||
* Receive transport msg.
|
||||
*/
|
||||
|
||||
e.receive = (msg: CollabAction) => {
|
||||
switch (msg.type) {
|
||||
case 'operation':
|
||||
return e.receiveOperation(msg.payload)
|
||||
case 'document':
|
||||
return e.receiveDocument(msg.payload)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send message to socket.
|
||||
*/
|
||||
|
||||
e.send = (msg: CollabAction) => {
|
||||
e.socket.emit('msg', msg)
|
||||
}
|
||||
|
||||
/**
|
||||
* Close socket and connection.
|
||||
*/
|
||||
|
||||
e.destroy = () => {
|
||||
e.socket.close()
|
||||
|
||||
e.closeConnection()
|
||||
}
|
||||
|
||||
autoConnect && e.connect()
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
export default withSocketIO
|
||||
Reference in New Issue
Block a user