feat: update to slate 0.5x (#10)

Update Slate-Collaboration to be compatible with Slate 0.5x versions.
This commit is contained in:
George
2020-05-10 16:50:12 +03:00
committed by GitHub
parent fee0098c3d
commit 0fd9390a99
79 changed files with 2017 additions and 1596 deletions

View File

@@ -1,23 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

View File

@@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@slate-collaborative/bridge": ["../../bridge"],
"@slate-collaborative/client": ["../../client"]
}
}
}

View File

@@ -1,18 +1,17 @@
{
"name": "@slate-collaborative/example",
"version": "0.0.1",
"version": "0.5.0",
"private": true,
"dependencies": {
"@emotion/core": "^10.0.17",
"@emotion/styled": "^10.0.17",
"@slate-collaborative/backend": "^0.0.3",
"@slate-collaborative/client": "^0.0.3",
"@slate-collaborative/backend": "^0.5.0",
"@slate-collaborative/client": "^0.5.0",
"@types/faker": "^4.1.5",
"@types/jest": "24.0.18",
"@types/node": "12.7.5",
"@types/react": "16.9.2",
"@types/react-dom": "16.9.0",
"@types/slate-react": "^0.22.5",
"@types/randomcolor": "^0.5.4",
"@types/react-dom": "^16.9.6",
"concurrently": "^4.1.2",
"cross-env": "^6.0.3",
"express": "^4.17.1",
@@ -23,14 +22,15 @@
"react": "^16.9.0",
"react-dom": "^16.9.0",
"react-scripts": "3.1.2",
"slate": "^0.47.8",
"slate-react": "^0.22.8",
"typescript": "3.6.3"
"slate": "^0.57.2",
"slate-history": "^0.57.2",
"slate-react": "^0.57.2",
"typescript": "^3.8.3"
},
"scripts": {
"start": "node server.js",
"start:cra": "react-scripts start",
"build": "cross-env NODE_ENV=production && react-scripts build",
"build:example": "cross-env NODE_ENV=production && react-scripts build",
"dev": "concurrently \"yarn start:cra\" \"yarn serve\"",
"serve": "nodemon --watch ../backend/lib --inspect server.js"
},

View File

@@ -10,7 +10,13 @@
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="logo192.png" />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/icon?family=Material+Icons"
/>
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>Slate collaborative</title>
</head>
<body>

View File

@@ -1,7 +1,17 @@
const Connection = require('@slate-collaborative/backend')
const defaultValue = require('./src/defaultValue')
const { SocketIOConnection } = require('@slate-collaborative/backend')
const express = require('express')
const defaultValue = [
{
type: 'paragraph',
children: [
{
text: 'Hello collaborator!'
}
]
}
]
const PORT = process.env.PORT || 9000
const server = express()
@@ -11,18 +21,19 @@ const server = express()
const config = {
entry: server, // or specify port to start io server
defaultValue,
saveTreshold: 2000,
saveFrequency: 2000,
onAuthRequest: async (query, socket) => {
// some query validation
return true
},
onDocumentLoad: async pathname => {
// return initial document ValueJSON by pathnme
// request initial document ValueJSON by pathnme
return defaultValue
},
onDocumentSave: async (pathname, document) => {
onDocumentSave: async (pathname, doc) => {
// save document
// console.log('onDocumentSave', pathname, doc)
}
}
const connection = new Connection(config)
const connection = new SocketIOConnection(config)

View File

@@ -1,53 +1,38 @@
import React, { Component } from 'react'
import React, { useState, useEffect } from 'react'
import faker from 'faker'
import styled from '@emotion/styled'
import Room from './Room'
class App extends Component<{}, { rooms: string[] }> {
state = {
rooms: []
}
const App = () => {
const [rooms, setRooms] = useState<string[]>([])
componentDidMount() {
this.addRoom()
}
const addRoom = () => setRooms(rooms.concat(faker.lorem.slug(4)))
render() {
const { rooms } = this.state
const removeRoom = (room: string) => () =>
setRooms(rooms.filter(r => r !== room))
return (
<Container>
<Panel>
<AddButton type="button" onClick={this.addRoom}>
Add Room
</AddButton>
</Panel>
{rooms.map(room => (
<Room key={room} slug={room} removeRoom={this.removeRoom(room)} />
))}
</Container>
)
}
useEffect(() => {
addRoom()
}, [])
addRoom = () => {
const room = faker.lorem.slug(4)
this.setState({ rooms: [...this.state.rooms, room] })
}
removeRoom = (room: string) => () => {
this.setState({
rooms: this.state.rooms.filter(r => r !== room)
})
}
return (
<div>
<Panel>
<AddButton type="button" onClick={addRoom}>
Add Room
</AddButton>
</Panel>
{rooms.map(room => (
<Room key={room} slug={room} removeRoom={removeRoom(room)} />
))}
</div>
)
}
export default App
const Container = styled.div``
const Panel = styled.div`
display: flex;
`

View File

@@ -0,0 +1,53 @@
import React from 'react'
interface Caret {
color: string
isForward: boolean
name: string
}
const Caret: React.FC<Caret> = ({ color, isForward, name }) => {
const cursorStyles = {
...cursorStyleBase,
background: color,
left: isForward ? '100%' : '0%'
}
const caretStyles = {
...caretStyleBase,
background: color,
left: isForward ? '100%' : '0%'
}
return (
<>
<span contentEditable={false} style={cursorStyles}>
{name}
</span>
<span contentEditable={false} style={caretStyles} />
</>
)
}
export default Caret
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: '1.2em',
width: 2,
background: 'palevioletred'
} as any

View File

@@ -1,39 +1,53 @@
import React, { Component } from 'react'
import React, { useState, useEffect, useMemo } from 'react'
import { createEditor, Node } from 'slate'
import { withHistory } from 'slate-history'
import { withReact } from 'slate-react'
import { Value, ValueJSON } from 'slate'
import { Editor } from 'slate-react'
import randomColor from 'randomcolor'
import styled from '@emotion/styled'
import ClientPlugin from '@slate-collaborative/client'
import { withIOCollaboration, useCursor } from '@slate-collaborative/client'
import defaultValue from './defaultValue'
import { Instance, Title, H4, Button } from './Elements'
import { Instance, ClientFrame, Title, H4, Button } from './elements'
import EditorFrame from './EditorFrame'
interface ClienProps {
const defaultValue: Node[] = [
{
type: 'paragraph',
children: [
{
text: ''
}
]
}
]
interface ClientProps {
name: string
id: string
slug: string
removeUser: (id: any) => void
}
class Client extends Component<ClienProps> {
editor: any
const Client: React.FC<ClientProps> = ({ id, name, slug, removeUser }) => {
const [value, setValue] = useState<Node[]>(defaultValue)
const [isOnline, setOnlineState] = useState<boolean>(false)
state = {
value: Value.fromJSON(defaultValue as ValueJSON),
isOnline: false,
plugins: []
}
const color = useMemo(
() =>
randomColor({
luminosity: 'dark',
format: 'rgba',
alpha: 1
}),
[]
)
componentDidMount() {
const color = randomColor({
luminosity: 'dark',
format: 'rgba',
alpha: 1
})
const editor = useMemo(() => {
const slateEditor = withReact(withHistory(createEditor()))
const origin =
process.env.NODE_ENV === 'production'
@@ -41,74 +55,60 @@ class Client extends Component<ClienProps> {
: 'http://localhost:9000'
const options = {
url: `${origin}/${this.props.slug}`,
connectOpts: {
query: {
name: this.props.name,
token: this.props.id,
slug: this.props.slug
}
},
annotationDataMixin: {
name: this.props.name,
docId: '/' + slug,
cursorData: {
name,
color,
alphaColor: color.slice(0, -2) + '0.2)'
},
// renderPreloader: () => <div>PRELOADER!!!!!!</div>,
onConnect: this.onConnect,
onDisconnect: this.onDisconnect
url: `${origin}/${slug}`,
connectOpts: {
query: {
name,
token: id,
slug
}
},
onConnect: () => setOnlineState(true),
onDisconnect: () => setOnlineState(false)
}
const plugin = ClientPlugin(options)
return withIOCollaboration(slateEditor, options)
}, [])
this.setState({
plugins: [plugin]
})
}
useEffect(() => {
editor.connect()
render() {
const { plugins, isOnline, value } = this.state
const { id, name } = this.props
return editor.destroy
}, [])
return (
<Instance online={isOnline}>
<Title>
<Head>Editor: {name}</Head>
<Button type="button" onClick={this.toggleOnline}>
Go {isOnline ? 'offline' : 'online'}
</Button>
<Button type="button" onClick={() => this.props.removeUser(id)}>
Remove
</Button>
</Title>
<ClientFrame>
<Editor
value={value}
ref={this.ref}
plugins={plugins}
onChange={this.onChange}
/>
</ClientFrame>
</Instance>
)
}
onChange = ({ value }: any) => this.setState({ value })
onConnect = () => this.setState({ isOnline: true })
onDisconnect = () => this.setState({ isOnline: false })
ref = node => {
this.editor = node
}
toggleOnline = () => {
const { isOnline } = this.state
const { connect, disconnect } = this.editor.connection
const { decorate } = useCursor(editor)
const toggleOnline = () => {
const { connect, disconnect } = editor
isOnline ? disconnect() : connect()
}
return (
<Instance online={isOnline}>
<Title>
<Head>Editor: {name}</Head>
<Button type="button" onClick={toggleOnline}>
Go {isOnline ? 'offline' : 'online'}
</Button>
<Button type="button" onClick={() => removeUser(id)}>
Remove
</Button>
</Title>
<EditorFrame
editor={editor}
value={value}
decorate={decorate}
onChange={(value: Node[]) => setValue(value)}
/>
</Instance>
)
}
export default Client

View File

@@ -0,0 +1,193 @@
import React, { useCallback } from 'react'
import { Transforms, Editor, Node } from 'slate'
import {
Slate,
ReactEditor,
Editable,
RenderLeafProps,
useSlate
} from 'slate-react'
import { ClientFrame, IconButton, Icon } from './Elements'
import Caret from './Caret'
const LIST_TYPES = ['numbered-list', 'bulleted-list']
export interface EditorFrame {
editor: ReactEditor
value: Node[]
decorate: any
onChange: (value: Node[]) => void
}
const renderElement = (props: any) => <Element {...props} />
const EditorFrame: React.FC<EditorFrame> = ({
editor,
value,
decorate,
onChange
}) => {
const renderLeaf = useCallback((props: any) => <Leaf {...props} />, [
decorate
])
return (
<ClientFrame>
<Slate editor={editor} value={value} onChange={onChange}>
<div
style={{
display: 'flex',
flexWrap: 'wrap',
position: 'sticky',
top: 0,
backgroundColor: 'white',
zIndex: 1
}}
>
<MarkButton format="bold" icon="format_bold" />
<MarkButton format="italic" icon="format_italic" />
<MarkButton format="underline" icon="format_underlined" />
<MarkButton format="code" icon="code" />
<BlockButton format="heading-one" icon="looks_one" />
<BlockButton format="heading-two" icon="looks_two" />
<BlockButton format="block-quote" icon="format_quote" />
<BlockButton format="numbered-list" icon="format_list_numbered" />
<BlockButton format="bulleted-list" icon="format_list_bulleted" />
</div>
<Editable
renderElement={renderElement}
renderLeaf={renderLeaf}
decorate={decorate}
/>
</Slate>
</ClientFrame>
)
}
export default EditorFrame
const toggleBlock = (editor: any, format: any) => {
const isActive = isBlockActive(editor, format)
const isList = LIST_TYPES.includes(format)
Transforms.unwrapNodes(editor, {
match: n => LIST_TYPES.includes(n.type),
split: true
})
Transforms.setNodes(editor, {
type: isActive ? 'paragraph' : isList ? 'list-item' : format
})
if (!isActive && isList) {
const block = { type: format, children: [] }
Transforms.wrapNodes(editor, block)
}
}
const toggleMark = (editor: any, format: any) => {
const isActive = isMarkActive(editor, format)
if (isActive) {
Editor.removeMark(editor, format)
} else {
Editor.addMark(editor, format, true)
}
}
const isBlockActive = (editor: any, format: any) => {
const [match] = Editor.nodes(editor, {
match: n => n.type === format
})
return !!match
}
const isMarkActive = (editor: any, format: any) => {
const marks = Editor.marks(editor)
return marks ? marks[format] === true : false
}
const Element: React.FC<any> = ({ attributes, children, element }) => {
switch (element.type) {
case 'block-quote':
return <blockquote {...attributes}>{children}</blockquote>
case 'bulleted-list':
return <ul {...attributes}>{children}</ul>
case 'heading-one':
return <h1 {...attributes}>{children}</h1>
case 'heading-two':
return <h2 {...attributes}>{children}</h2>
case 'list-item':
return <li {...attributes}>{children}</li>
case 'numbered-list':
return <ol {...attributes}>{children}</ol>
default:
return <p {...attributes}>{children}</p>
}
}
const Leaf: React.FC<RenderLeafProps> = ({ attributes, children, leaf }) => {
if (leaf.bold) {
children = <strong>{children}</strong>
}
if (leaf.code) {
children = <code>{children}</code>
}
if (leaf.italic) {
children = <em>{children}</em>
}
if (leaf.underline) {
children = <u>{children}</u>
}
return (
<span
{...attributes}
style={{
position: 'relative',
backgroundColor: leaf.alphaColor
}}
>
{leaf.isCaret ? <Caret {...(leaf as any)} /> : null}
{children}
</span>
)
}
const BlockButton: React.FC<any> = ({ format, icon }) => {
const editor = useSlate()
return (
<IconButton
active={isBlockActive(editor, format)}
onMouseDown={event => {
event.preventDefault()
toggleBlock(editor, format)
}}
>
<Icon className="material-icons">{icon}</Icon>
</IconButton>
)
}
const MarkButton: React.FC<any> = ({ format, icon }) => {
const editor = useSlate()
return (
<IconButton
active={isMarkActive(editor, format)}
onMouseDown={event => {
event.preventDefault()
toggleMark(editor, format)
}}
>
<Icon className="material-icons">{icon}</Icon>
</IconButton>
)
}

View File

@@ -1,8 +1,9 @@
import React, { Component, ChangeEvent } from 'react'
import React, { useState, ChangeEvent } from 'react'
import faker from 'faker'
import debounce from 'lodash/debounce'
import { RoomWrapper, H4, Title, Button, Grid, Input } from './elements'
import { RoomWrapper, H4, Title, Button, Grid, Input } from './Elements'
import Client from './Client'
@@ -16,78 +17,57 @@ interface RoomProps {
removeRoom: () => void
}
interface RoomState {
users: User[]
slug: string
rebuild: boolean
}
const createUser = (): User => ({
id: faker.random.uuid(),
name: `${faker.name.firstName()} ${faker.name.lastName()}`
})
class Room extends Component<RoomProps, RoomState> {
state = {
users: [],
slug: this.props.slug,
rebuild: false
}
const Room: React.FC<RoomProps> = ({ slug, removeRoom }) => {
const [users, setUsers] = useState<User[]>([createUser(), createUser()])
const [roomSlug, setRoomSlug] = useState<string>(slug)
const [isRemounted, setRemountState] = useState(false)
componentDidMount() {
this.addUser()
setTimeout(this.addUser, 10)
}
render() {
const { users, slug, rebuild } = this.state
return (
<RoomWrapper>
<Title>
<H4>Document slug:</H4>
<Input type="text" value={slug} onChange={this.changeSlug} />
<Button type="button" onClick={this.addUser}>
Add random user
</Button>
<Button type="button" onClick={this.props.removeRoom}>
Remove Room
</Button>
</Title>
<Grid>
{users.map(
(user: User) =>
!rebuild && (
<Client
{...user}
slug={slug}
key={user.id}
removeUser={this.removeUser}
/>
)
)}
</Grid>
</RoomWrapper>
)
}
addUser = () => {
const user = {
id: faker.random.uuid(),
name: `${faker.name.firstName()} ${faker.name.lastName()}`
}
this.setState({ users: [...this.state.users, user] })
}
removeUser = (userId: string) => {
this.setState({
users: this.state.users.filter((u: User) => u.id !== userId)
})
}
changeSlug = (e: ChangeEvent<HTMLInputElement>) => {
this.setState({ slug: e.target.value }, this.rebuildClient)
}
rebuildClient = debounce(() => {
this.setState({ rebuild: true }, () => this.setState({ rebuild: false }))
const remount = debounce(() => {
setRemountState(true)
setTimeout(setRemountState, 50, false)
}, 300)
const changeSlug = (e: ChangeEvent<HTMLInputElement>) => {
setRoomSlug(e.target.value)
remount()
}
const addUser = () => setUsers(users => users.concat(createUser()))
const removeUser = (userId: string) =>
setUsers(users => users.filter((u: User) => u.id !== userId))
return (
<RoomWrapper>
<Title>
<H4>Document slug:</H4>
<Input type="text" value={roomSlug} onChange={changeSlug} />
<Button type="button" onClick={addUser}>
Add random user
</Button>
<Button type="button" onClick={removeRoom}>
Remove Room
</Button>
</Title>
<Grid>
{users.map((user: User) =>
isRemounted ? null : (
<Client
{...user}
slug={roomSlug}
key={user.id}
removeUser={removeUser}
/>
)
)}
</Grid>
</RoomWrapper>
)
}
export default Room

View File

@@ -1,17 +0,0 @@
module.exports = {
document: {
nodes: [
{
object: 'block',
type: 'paragraph',
nodes: [
{
object: 'text',
marks: [],
text: 'Hello collaborator!'
}
]
}
]
}
}

View File

@@ -36,6 +36,14 @@ export const Button = styled.button`
}
`
export const IconButton = styled(Button)((props: any) => ({
color: props.active ? 'mediumvioletred' : 'lightpink',
border: 'none',
padding: 0
}))
export const Icon = styled.div``
export const Grid = styled.div`
display: grid;
grid-gap: 2vw;
@@ -61,4 +69,12 @@ export const ClientFrame = styled.div`
margin-left: -10px;
margin-right: -10px;
background: white;
blockquote {
border-left: 2px solid #ddd;
margin-left: 0;
margin-right: 0;
padding-left: 10px;
color: #aaa;
font-style: italic;
}
`

View File

@@ -2,32 +2,25 @@
"include": [
"src/**/*"
],
"extends": "./extend.tsconfig.json",
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"baseUrl": "src",
"jsx": "react",
"rootDir": "../",
"baseUrl": "./src",
"allowJs": true,
"declaration": false,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"noEmit": true,
"isolatedModules": true,
"skipLibCheck": true,
"noImplicitAny": false,
"removeComments": true,
"noLib": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"sourceMap": true
}
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"resolveJsonModule": true,
"declaration": false,
"declarationMap": false,
"noEmit": true
},
"references": [
{
"path": "../client"
},
{
"path": "../backend"
}
]
}