mirror of
https://github.com/cudr/slate-collaborative.git
synced 2024-10-27 20:34:06 +00:00
Implement link editor (#20)
* feat: implement link editor * feat: update slate to 0.58.3
This commit is contained in:
parent
ce0c86fb3c
commit
eb370eaa1e
@ -31,7 +31,7 @@
|
|||||||
"@types/socket.io": "^2.1.4",
|
"@types/socket.io": "^2.1.4",
|
||||||
"automerge": "0.14.0",
|
"automerge": "0.14.0",
|
||||||
"lodash": "^4.17.15",
|
"lodash": "^4.17.15",
|
||||||
"slate": "0.58.0",
|
"slate": "0.58.3",
|
||||||
"socket.io": "^2.3.0",
|
"socket.io": "^2.3.0",
|
||||||
"typescript": "^3.8.3"
|
"typescript": "^3.8.3"
|
||||||
},
|
},
|
||||||
|
@ -26,7 +26,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"automerge": "0.14.0",
|
"automerge": "0.14.0",
|
||||||
"slate": "0.58.0",
|
"slate": "0.58.3",
|
||||||
"typescript": "^3.8.3"
|
"typescript": "^3.8.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -28,7 +28,7 @@
|
|||||||
"@babel/preset-react": "^7.0.0",
|
"@babel/preset-react": "^7.0.0",
|
||||||
"@slate-collaborative/bridge": "0.6.1",
|
"@slate-collaborative/bridge": "0.6.1",
|
||||||
"automerge": "0.14.0",
|
"automerge": "0.14.0",
|
||||||
"slate": "0.58.0",
|
"slate": "0.58.3",
|
||||||
"socket.io-client": "^2.3.0",
|
"socket.io-client": "^2.3.0",
|
||||||
"typescript": "^3.8.3"
|
"typescript": "^3.8.3"
|
||||||
},
|
},
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
"@slate-collaborative/backend": "^0.6.2",
|
"@slate-collaborative/backend": "^0.6.2",
|
||||||
"@slate-collaborative/client": "^0.6.2",
|
"@slate-collaborative/client": "^0.6.2",
|
||||||
"@types/faker": "^4.1.5",
|
"@types/faker": "^4.1.5",
|
||||||
|
"@types/is-url": "^1.2.28",
|
||||||
"@types/jest": "24.0.18",
|
"@types/jest": "24.0.18",
|
||||||
"@types/node": "12.7.5",
|
"@types/node": "12.7.5",
|
||||||
"@types/randomcolor": "^0.5.4",
|
"@types/randomcolor": "^0.5.4",
|
||||||
@ -16,15 +17,16 @@
|
|||||||
"cross-env": "^6.0.3",
|
"cross-env": "^6.0.3",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"faker": "^4.1.0",
|
"faker": "^4.1.0",
|
||||||
|
"is-url": "^1.2.4",
|
||||||
"lodash": "^4.17.15",
|
"lodash": "^4.17.15",
|
||||||
"nodemon": "^1.19.2",
|
"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",
|
||||||
"react-scripts": "3.1.2",
|
"react-scripts": "3.1.2",
|
||||||
"slate": "0.58.0",
|
"slate": "0.58.3",
|
||||||
"slate-history": "0.58.0",
|
"slate-history": "0.58.3",
|
||||||
"slate-react": "0.58.0",
|
"slate-react": "0.58.3",
|
||||||
"typescript": "^3.8.3"
|
"typescript": "^3.8.3"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -14,6 +14,8 @@ import { Instance, Title, H4, Button } from './Components'
|
|||||||
|
|
||||||
import EditorFrame from './EditorFrame'
|
import EditorFrame from './EditorFrame'
|
||||||
|
|
||||||
|
import { withLinks } from './plugins/link'
|
||||||
|
|
||||||
const defaultValue: Node[] = [
|
const defaultValue: Node[] = [
|
||||||
{
|
{
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
@ -47,7 +49,7 @@ const Client: React.FC<ClientProps> = ({ id, name, slug, removeUser }) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const editor = useMemo(() => {
|
const editor = useMemo(() => {
|
||||||
const slateEditor = withReact(withHistory(createEditor()))
|
const slateEditor = withLinks(withReact(withHistory(createEditor())))
|
||||||
|
|
||||||
const origin =
|
const origin =
|
||||||
process.env.NODE_ENV === 'production'
|
process.env.NODE_ENV === 'production'
|
||||||
|
@ -83,4 +83,11 @@ export const ClientFrame = styled.div`
|
|||||||
color: #aaa;
|
color: #aaa;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
a {
|
||||||
|
color: purple;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
a:visited {
|
||||||
|
color: darkmagenta;
|
||||||
|
}
|
||||||
`
|
`
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import React, { useCallback } from 'react'
|
import React, { useCallback } from 'react'
|
||||||
|
|
||||||
import { Transforms, Editor, Node } from 'slate'
|
import { Node } from 'slate'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Slate,
|
Slate,
|
||||||
ReactEditor,
|
ReactEditor,
|
||||||
@ -13,7 +14,9 @@ import { ClientFrame, IconButton, Icon } from './Components'
|
|||||||
|
|
||||||
import Caret from './Caret'
|
import Caret from './Caret'
|
||||||
|
|
||||||
const LIST_TYPES: string[] = ['numbered-list', 'bulleted-list']
|
import { isBlockActive, toggleBlock } from './plugins/block'
|
||||||
|
import { isMarkActive, toggleMark } from './plugins/mark'
|
||||||
|
import { isLinkActive, insertLink, unwrapLink } from './plugins/link'
|
||||||
|
|
||||||
export interface EditorFrame {
|
export interface EditorFrame {
|
||||||
editor: ReactEditor
|
editor: ReactEditor
|
||||||
@ -51,11 +54,15 @@ const EditorFrame: React.FC<EditorFrame> = ({
|
|||||||
<MarkButton format="italic" icon="format_italic" />
|
<MarkButton format="italic" icon="format_italic" />
|
||||||
<MarkButton format="underline" icon="format_underlined" />
|
<MarkButton format="underline" icon="format_underlined" />
|
||||||
<MarkButton format="code" icon="code" />
|
<MarkButton format="code" icon="code" />
|
||||||
|
|
||||||
<BlockButton format="heading-one" icon="looks_one" />
|
<BlockButton format="heading-one" icon="looks_one" />
|
||||||
<BlockButton format="heading-two" icon="looks_two" />
|
<BlockButton format="heading-two" icon="looks_two" />
|
||||||
<BlockButton format="block-quote" icon="format_quote" />
|
<BlockButton format="block-quote" icon="format_quote" />
|
||||||
|
|
||||||
<BlockButton format="numbered-list" icon="format_list_numbered" />
|
<BlockButton format="numbered-list" icon="format_list_numbered" />
|
||||||
<BlockButton format="bulleted-list" icon="format_list_bulleted" />
|
<BlockButton format="bulleted-list" icon="format_list_bulleted" />
|
||||||
|
|
||||||
|
<LinkButton />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Editable
|
<Editable
|
||||||
@ -70,50 +77,14 @@ const EditorFrame: React.FC<EditorFrame> = ({
|
|||||||
|
|
||||||
export default EditorFrame
|
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 as any),
|
|
||||||
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 }) => {
|
const Element: React.FC<any> = ({ attributes, children, element }) => {
|
||||||
switch (element.type) {
|
switch (element.type) {
|
||||||
|
case 'link':
|
||||||
|
return (
|
||||||
|
<a {...attributes} href={element.href}>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
)
|
||||||
case 'block-quote':
|
case 'block-quote':
|
||||||
return <blockquote {...attributes}>{children}</blockquote>
|
return <blockquote {...attributes}>{children}</blockquote>
|
||||||
case 'bulleted-list':
|
case 'bulleted-list':
|
||||||
@ -193,3 +164,26 @@ const MarkButton: React.FC<any> = ({ format, icon }) => {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const LinkButton = () => {
|
||||||
|
const editor = useSlate()
|
||||||
|
|
||||||
|
const isActive = isLinkActive(editor)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
active={isActive}
|
||||||
|
onMouseDown={event => {
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
if (isActive) return unwrapLink(editor)
|
||||||
|
|
||||||
|
const url = window.prompt('Enter the URL of the link:')
|
||||||
|
|
||||||
|
url && insertLink(editor, url)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon className="material-icons">link</Icon>
|
||||||
|
</IconButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
30
packages/example/src/plugins/block.ts
Normal file
30
packages/example/src/plugins/block.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { Transforms, Editor } from 'slate'
|
||||||
|
|
||||||
|
const LIST_TYPES: string[] = ['numbered-list', 'bulleted-list']
|
||||||
|
|
||||||
|
export 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 as any),
|
||||||
|
split: true
|
||||||
|
})
|
||||||
|
|
||||||
|
Transforms.setNodes(editor, {
|
||||||
|
type: isActive ? 'paragraph' : isList ? 'list-item' : format
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!isActive && isList) {
|
||||||
|
const block = { type: format, children: [] }
|
||||||
|
Transforms.wrapNodes(editor, block)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isBlockActive = (editor: any, format: any) => {
|
||||||
|
const [match] = Editor.nodes(editor, {
|
||||||
|
match: n => n.type === format
|
||||||
|
})
|
||||||
|
|
||||||
|
return !!match
|
||||||
|
}
|
73
packages/example/src/plugins/link.ts
Normal file
73
packages/example/src/plugins/link.ts
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import isUrl from 'is-url'
|
||||||
|
|
||||||
|
import { Transforms, Editor, Range } from 'slate'
|
||||||
|
|
||||||
|
export interface LinkEditor extends Editor {
|
||||||
|
insertData: (data: any) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const withLinks = <T extends Editor>(editor: T) => {
|
||||||
|
const e = editor as T & LinkEditor
|
||||||
|
|
||||||
|
const { insertData, insertText, isInline } = e
|
||||||
|
|
||||||
|
e.isInline = (element: any) => {
|
||||||
|
return element.type === 'link' ? true : isInline(element)
|
||||||
|
}
|
||||||
|
|
||||||
|
e.insertText = (text: string) => {
|
||||||
|
if (text && isUrl(text)) {
|
||||||
|
wrapLink(editor, text)
|
||||||
|
} else {
|
||||||
|
insertText(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
e.insertData = (data: any) => {
|
||||||
|
const text = data.getData('text/plain')
|
||||||
|
|
||||||
|
if (text && isUrl(text)) {
|
||||||
|
wrapLink(editor, text)
|
||||||
|
} else {
|
||||||
|
insertData(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return editor
|
||||||
|
}
|
||||||
|
|
||||||
|
export const insertLink = (editor: Editor, href: string) => {
|
||||||
|
if (editor.selection) {
|
||||||
|
wrapLink(editor, href)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isLinkActive = (editor: Editor) => {
|
||||||
|
const [link] = Editor.nodes(editor, { match: n => n.type === 'link' })
|
||||||
|
return !!link
|
||||||
|
}
|
||||||
|
|
||||||
|
export const unwrapLink = (editor: Editor) => {
|
||||||
|
Transforms.unwrapNodes(editor, { match: n => n.type === 'link' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const wrapLink = (editor: Editor, href: string) => {
|
||||||
|
if (isLinkActive(editor)) {
|
||||||
|
unwrapLink(editor)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { selection } = editor
|
||||||
|
const isCollapsed = selection && Range.isCollapsed(selection)
|
||||||
|
const link = {
|
||||||
|
type: 'link',
|
||||||
|
href,
|
||||||
|
children: isCollapsed ? [{ text: href }] : []
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCollapsed) {
|
||||||
|
Transforms.insertNodes(editor, link)
|
||||||
|
} else {
|
||||||
|
Transforms.wrapNodes(editor, link, { split: true })
|
||||||
|
Transforms.collapse(editor, { edge: 'end' })
|
||||||
|
}
|
||||||
|
}
|
16
packages/example/src/plugins/mark.ts
Normal file
16
packages/example/src/plugins/mark.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { Editor } from 'slate'
|
||||||
|
|
||||||
|
export const toggleMark = (editor: Editor, format: any) => {
|
||||||
|
const isActive = isMarkActive(editor, format)
|
||||||
|
|
||||||
|
if (isActive) {
|
||||||
|
Editor.removeMark(editor, format)
|
||||||
|
} else {
|
||||||
|
Editor.addMark(editor, format, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isMarkActive = (editor: Editor, format: any) => {
|
||||||
|
const marks = Editor.marks(editor)
|
||||||
|
return marks ? marks[format] === true : false
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user