mirror of
https://github.com/cudr/slate-collaborative.git
synced 2024-10-27 20:34:06 +00:00
feat: extract cursor component
This commit is contained in:
parent
0ceb38bbfd
commit
21a4a7d99a
@ -118,6 +118,8 @@ class Connection {
|
|||||||
this.connections[id].receiveMsg(data)
|
this.connections[id].receiveMsg(data)
|
||||||
|
|
||||||
this.saveDoc(name)
|
this.saveDoc(name)
|
||||||
|
|
||||||
|
this.garbageCursors(name)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e)
|
console.log(e)
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Operation, SyncDoc } from '../model'
|
import { Operation, SyncDoc } from '../model/index'
|
||||||
|
|
||||||
export const addAnnotation = (doc: SyncDoc, op: Operation): SyncDoc => {
|
export const addAnnotation = (doc: SyncDoc, op: Operation): SyncDoc => {
|
||||||
console.log('addAnnotation!!!', op.toJS())
|
console.log('addAnnotation!!!', op.toJS())
|
||||||
|
@ -21,7 +21,10 @@ export const applyOperation = (doc: SyncDoc, op: Operation): SyncDoc => {
|
|||||||
try {
|
try {
|
||||||
const applyOp = opType[op.type]
|
const applyOp = opType[op.type]
|
||||||
|
|
||||||
if (!applyOp) throw new TypeError('Unsupported operation type!')
|
if (!applyOp) {
|
||||||
|
console.log('operation', op.toJS())
|
||||||
|
throw new TypeError(`Unsupported operation type: ${op.type}!`)
|
||||||
|
}
|
||||||
|
|
||||||
return applyOp(doc, op)
|
return applyOp(doc, op)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -30,9 +30,7 @@ const toSlateOp = (ops: Automerge.Diff[], doc) => {
|
|||||||
[]
|
[]
|
||||||
])
|
])
|
||||||
|
|
||||||
const res = defer.flatMap(op => op(tempTree, doc)).filter(op => op)
|
return defer.flatMap(op => op(tempTree, doc)).filter(op => op)
|
||||||
|
|
||||||
return res
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export { toSlateOp }
|
export { toSlateOp }
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Operation, Selection } from 'slate'
|
import { Operation, Selection } from 'slate'
|
||||||
import { List } from 'immutable'
|
import * as Immutable from 'immutable'
|
||||||
import merge from 'lodash/merge'
|
import merge from 'lodash/merge'
|
||||||
|
|
||||||
import { toJS } from '../utils'
|
import { toJS } from '../utils'
|
||||||
@ -51,7 +51,7 @@ export const removeCursor = (doc: SyncDoc, key: CursorKey) => {
|
|||||||
return doc
|
return doc
|
||||||
}
|
}
|
||||||
|
|
||||||
export const cursorOpFilter = (ops: List<Operation>, type: string) =>
|
export const cursorOpFilter = (ops: Immutable.List<Operation>, type: string) =>
|
||||||
ops.filter(op => {
|
ops.filter(op => {
|
||||||
if (op.type === 'set_annotation') {
|
if (op.type === 'set_annotation') {
|
||||||
return !(
|
return !(
|
||||||
|
47
packages/client/src/Cursor.tsx
Normal file
47
packages/client/src/Cursor.tsx
Normal file
@ -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
|
@ -2,23 +2,18 @@ import onChange from './onChange'
|
|||||||
import renderEditor from './renderEditor'
|
import renderEditor from './renderEditor'
|
||||||
import renderAnnotation from './renderAnnotation'
|
import renderAnnotation from './renderAnnotation'
|
||||||
|
|
||||||
|
import renderCursor from './renderCursor'
|
||||||
|
|
||||||
import { PluginOptions } from './model'
|
import { PluginOptions } from './model'
|
||||||
|
|
||||||
export const defaultOpts = {
|
export const defaultOpts = {
|
||||||
url: 'http://localhost:9000',
|
url: 'http://localhost:9000',
|
||||||
cursorAnnotationType: 'collaborative_selection',
|
cursorAnnotationType: 'collaborative_selection',
|
||||||
|
renderCursor,
|
||||||
annotationDataMixin: {
|
annotationDataMixin: {
|
||||||
name: 'an collaborator'
|
name: 'an collaborator name',
|
||||||
},
|
color: 'palevioletred',
|
||||||
renderCursor: data => data.name,
|
alphaColor: 'rgba(233, 30, 99, 0.2)'
|
||||||
cursorStyle: {
|
|
||||||
background: 'palevioletred'
|
|
||||||
},
|
|
||||||
caretStyle: {
|
|
||||||
background: 'palevioletred'
|
|
||||||
},
|
|
||||||
selectionStyle: {
|
|
||||||
background: 'rgba(233, 30, 99, 0.2)'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,9 +1,12 @@
|
|||||||
import { ReactNode } from 'react'
|
import { ReactNode } from 'react'
|
||||||
import CSS from 'csstype'
|
|
||||||
import { Editor, Controller, Value } from 'slate'
|
import { Editor, Controller, Value } from 'slate'
|
||||||
|
|
||||||
import Connection from './Connection'
|
import Connection from './Connection'
|
||||||
|
|
||||||
|
type Data = {
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
interface FixedController extends Controller {
|
interface FixedController extends Controller {
|
||||||
setValue: (value: Value) => void
|
setValue: (value: Value) => void
|
||||||
}
|
}
|
||||||
@ -32,13 +35,9 @@ export interface PluginOptions {
|
|||||||
url?: string
|
url?: string
|
||||||
connectOpts?: SocketIOClient.ConnectOpts
|
connectOpts?: SocketIOClient.ConnectOpts
|
||||||
cursorAnnotationType?: string
|
cursorAnnotationType?: string
|
||||||
caretStyle?: CSS.Properties
|
annotationDataMixin?: Data
|
||||||
cursorStyle?: CSS.Properties
|
|
||||||
renderPreloader?: () => ReactNode
|
renderPreloader?: () => ReactNode
|
||||||
annotationDataMixin?: {
|
renderCursor?: (data: Data) => ReactNode | any
|
||||||
[key: string]: any
|
|
||||||
}
|
|
||||||
renderCursor?: (data: any) => ReactNode | string | any
|
|
||||||
onConnect?: (connection: Connection) => void
|
onConnect?: (connection: Connection) => void
|
||||||
onDisconnect?: (connection: Connection) => void
|
onDisconnect?: (connection: Connection) => void
|
||||||
}
|
}
|
||||||
|
@ -1,73 +1,28 @@
|
|||||||
import React, { Fragment } from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
const wrapStyles = {
|
const renderAnnotation = ({ cursorAnnotationType, renderCursor }) => (
|
||||||
background: 'rgba(233, 30, 99, 0.2)',
|
props,
|
||||||
position: 'relative'
|
editor,
|
||||||
}
|
next
|
||||||
|
) => {
|
||||||
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 renderAnnotation = ({
|
|
||||||
cursorAnnotationType,
|
|
||||||
renderCursor,
|
|
||||||
cursorStyle = {},
|
|
||||||
caretStyle = {},
|
|
||||||
selectionStyle = {}
|
|
||||||
}) => (props, editor, next) => {
|
|
||||||
const { children, annotation, attributes, node } = props
|
const { children, annotation, attributes, node } = props
|
||||||
|
|
||||||
if (annotation.type !== cursorAnnotationType) return next()
|
if (annotation.type !== cursorAnnotationType) return next()
|
||||||
|
|
||||||
const isBackward = annotation.data.get('isBackward')
|
const data = annotation.data.toJS()
|
||||||
const targetPath = annotation.data.get('targetPath')
|
|
||||||
const cursorText = renderCursor(annotation.data)
|
|
||||||
|
|
||||||
const cursorStyles = {
|
|
||||||
...cursorStyleBase,
|
|
||||||
...cursorStyle,
|
|
||||||
left: isBackward ? '0%' : '100%'
|
|
||||||
}
|
|
||||||
const caretStyles = {
|
|
||||||
...caretStyleBase,
|
|
||||||
...caretStyle,
|
|
||||||
left: isBackward ? '0%' : '100%'
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const { targetPath, alphaColor } = data
|
||||||
const { document } = editor.value
|
const { document } = editor.value
|
||||||
|
|
||||||
const targetNode = document.getNode(targetPath)
|
const targetNode = document.getNode(targetPath)
|
||||||
const isShowCursor = targetNode && targetNode.key === node.key
|
const showCursor = targetNode && targetNode.key === node.key
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span {...attributes} style={{ ...wrapStyles, ...selectionStyle }}>
|
<span
|
||||||
{isShowCursor ? (
|
{...attributes}
|
||||||
<Fragment>
|
style={{ position: 'relative', background: alphaColor }}
|
||||||
<span contentEditable={false} style={cursorStyles}>
|
>
|
||||||
{cursorText}
|
{showCursor ? renderCursor(data) : null}
|
||||||
</span>
|
|
||||||
<span contentEditable={false} style={caretStyles} />
|
|
||||||
</Fragment>
|
|
||||||
) : null}
|
|
||||||
{children}
|
{children}
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
|
7
packages/client/src/renderCursor.tsx
Normal file
7
packages/client/src/renderCursor.tsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import Cursor from './Cursor'
|
||||||
|
|
||||||
|
const renderCursor = data => <Cursor {...data} />
|
||||||
|
|
||||||
|
export default renderCursor
|
@ -16,6 +16,7 @@
|
|||||||
"concurrently": "^4.1.2",
|
"concurrently": "^4.1.2",
|
||||||
"faker": "^4.1.0",
|
"faker": "^4.1.0",
|
||||||
"lodash": "^4.17.15",
|
"lodash": "^4.17.15",
|
||||||
|
"nodemon": "^1.19.2",
|
||||||
"randomcolor": "^0.5.4",
|
"randomcolor": "^0.5.4",
|
||||||
"react": "^16.9.0",
|
"react": "^16.9.0",
|
||||||
"react-dom": "^16.9.0",
|
"react-dom": "^16.9.0",
|
||||||
@ -26,10 +27,9 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"start": "react-scripts start",
|
||||||
|
"build": "react-scripts build",
|
||||||
"dev": "concurrently \"yarn start\" \"yarn serve\"",
|
"dev": "concurrently \"yarn start\" \"yarn serve\"",
|
||||||
"serve": "nodemon --watch ../backend/lib --inspect server.js",
|
"serve": "nodemon --watch ../backend/lib --inspect server.js"
|
||||||
"test": "react-scripts test",
|
|
||||||
"eject": "react-scripts eject"
|
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": "react-app"
|
"extends": "react-app"
|
||||||
@ -45,8 +45,5 @@
|
|||||||
"last 1 firefox version",
|
"last 1 firefox version",
|
||||||
"last 1 safari version"
|
"last 1 safari version"
|
||||||
]
|
]
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"nodemon": "^1.19.2"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,9 +19,11 @@ class App extends Component<{}, { rooms: string[] }> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<AddButton type="button" onClick={this.addRoom}>
|
<Panel>
|
||||||
Add Room
|
<AddButton type="button" onClick={this.addRoom}>
|
||||||
</AddButton>
|
Add Room
|
||||||
|
</AddButton>
|
||||||
|
</Panel>
|
||||||
{rooms.map(room => (
|
{rooms.map(room => (
|
||||||
<Room key={room} slug={room} removeRoom={this.removeRoom(room)} />
|
<Room key={room} slug={room} removeRoom={this.removeRoom(room)} />
|
||||||
))}
|
))}
|
||||||
@ -46,6 +48,10 @@ export default App
|
|||||||
|
|
||||||
const Container = styled.div``
|
const Container = styled.div``
|
||||||
|
|
||||||
|
const Panel = styled.div`
|
||||||
|
display: flex;
|
||||||
|
`
|
||||||
|
|
||||||
const Button = styled.button`
|
const Button = styled.button`
|
||||||
padding: 6px 14px;
|
padding: 6px 14px;
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -45,18 +45,10 @@ class Client extends Component<ClienProps> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
annotationDataMixin: {
|
annotationDataMixin: {
|
||||||
name: this.props.name
|
name: this.props.name,
|
||||||
|
color,
|
||||||
|
alphaColor: color.slice(0, -2) + '0.2)'
|
||||||
},
|
},
|
||||||
cursorStyle: {
|
|
||||||
background: color
|
|
||||||
},
|
|
||||||
caretStyle: {
|
|
||||||
background: color
|
|
||||||
},
|
|
||||||
selectionStyle: {
|
|
||||||
background: color.slice(0, -2) + '0.2)'
|
|
||||||
},
|
|
||||||
renderCursor: data => data.get('name'),
|
|
||||||
// renderPreloader: () => <div>PRELOADER!!!!!!</div>,
|
// renderPreloader: () => <div>PRELOADER!!!!!!</div>,
|
||||||
onConnect: this.onConnect,
|
onConnect: this.onConnect,
|
||||||
onDisconnect: this.onDisconnect
|
onDisconnect: this.onDisconnect
|
||||||
|
Loading…
Reference in New Issue
Block a user