commit
31599caa71
@ -0,0 +1,69 @@
|
||||
import { Operation, Selection } from 'slate'
|
||||
import * as Immutable 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
|
||||
|
||||
if (!doc.annotations) {
|
||||
doc.annotations = {}
|
||||
}
|
||||
|
||||
if (!doc.annotations[key]) {
|
||||
doc.annotations[key] = {
|
||||
key,
|
||||
type,
|
||||
data: {}
|
||||
}
|
||||
}
|
||||
|
||||
const annotation = toJS(doc.annotations[key])
|
||||
|
||||
annotation.focus = selection.end.toJSON()
|
||||
annotation.anchor = selection.start.toJSON()
|
||||
|
||||
annotation.data = merge(annotation.data, data, {
|
||||
isBackward: selection.isBackward,
|
||||
targetPath: selection.isBackward
|
||||
? annotation.anchor.path
|
||||
: annotation.focus.path
|
||||
})
|
||||
|
||||
doc.annotations[key] = annotation
|
||||
|
||||
return doc
|
||||
}
|
||||
|
||||
export const removeCursor = (doc: SyncDoc, key: CursorKey) => {
|
||||
if (doc.annotations && doc.annotations[key]) {
|
||||
delete doc.annotations[key]
|
||||
}
|
||||
|
||||
return doc
|
||||
}
|
||||
|
||||
export const cursorOpFilter = (ops: Immutable.List<Operation>, type: string) =>
|
||||
ops.filter(op => {
|
||||
if (op.type === 'set_annotation') {
|
||||
return !(
|
||||
(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 !== type
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
@ -1,3 +1,5 @@
|
||||
export * from './apply'
|
||||
export * from './convert'
|
||||
export * from './utils'
|
||||
export * from './cursor'
|
||||
export * from './model'
|
||||
|
@ -1,3 +1,3 @@
|
||||
import { Doc } from 'automerge'
|
||||
export type CursorKey = string
|
||||
|
||||
export type SyncDoc = Doc<any>
|
||||
export type SyncDoc = any
|
||||
|
@ -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 (
|
||||
<Fragment>
|
||||
<span contentEditable={false} style={cursorStyles}>
|
||||
{name}
|
||||
</span>
|
||||
<span contentEditable={false} style={caretStyles} />
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
export default Cursor
|
@ -1,29 +0,0 @@
|
||||
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
|
@ -0,0 +1,30 @@
|
||||
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
|
@ -0,0 +1,31 @@
|
||||
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
|
@ -0,0 +1,7 @@
|
||||
import React from 'react'
|
||||
|
||||
import Cursor from './Cursor'
|
||||
|
||||
const renderCursor = data => <Cursor {...data} />
|
||||
|
||||
export default renderCursor
|
Loading…
Reference in new issue