Skip to content

Commit

Permalink
Implement link editor (#20)
Browse files Browse the repository at this point in the history
* feat: implement link editor

* feat: update slate to 0.58.3
  • Loading branch information
cudr authored Jun 21, 2020
1 parent ce0c86f commit eb370ea
Show file tree
Hide file tree
Showing 10 changed files with 175 additions and 51 deletions.
2 changes: 1 addition & 1 deletion packages/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"@types/socket.io": "^2.1.4",
"automerge": "0.14.0",
"lodash": "^4.17.15",
"slate": "0.58.0",
"slate": "0.58.3",
"socket.io": "^2.3.0",
"typescript": "^3.8.3"
},
Expand Down
2 changes: 1 addition & 1 deletion packages/bridge/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
},
"dependencies": {
"automerge": "0.14.0",
"slate": "0.58.0",
"slate": "0.58.3",
"typescript": "^3.8.3"
},
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"@babel/preset-react": "^7.0.0",
"@slate-collaborative/bridge": "0.6.1",
"automerge": "0.14.0",
"slate": "0.58.0",
"slate": "0.58.3",
"socket.io-client": "^2.3.0",
"typescript": "^3.8.3"
},
Expand Down
8 changes: 5 additions & 3 deletions packages/example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"@slate-collaborative/backend": "^0.6.2",
"@slate-collaborative/client": "^0.6.2",
"@types/faker": "^4.1.5",
"@types/is-url": "^1.2.28",
"@types/jest": "24.0.18",
"@types/node": "12.7.5",
"@types/randomcolor": "^0.5.4",
Expand All @@ -16,15 +17,16 @@
"cross-env": "^6.0.3",
"express": "^4.17.1",
"faker": "^4.1.0",
"is-url": "^1.2.4",
"lodash": "^4.17.15",
"nodemon": "^1.19.2",
"randomcolor": "^0.5.4",
"react": "^16.9.0",
"react-dom": "^16.9.0",
"react-scripts": "3.1.2",
"slate": "0.58.0",
"slate-history": "0.58.0",
"slate-react": "0.58.0",
"slate": "0.58.3",
"slate-history": "0.58.3",
"slate-react": "0.58.3",
"typescript": "^3.8.3"
},
"scripts": {
Expand Down
4 changes: 3 additions & 1 deletion packages/example/src/Client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { Instance, Title, H4, Button } from './Components'

import EditorFrame from './EditorFrame'

import { withLinks } from './plugins/link'

const defaultValue: Node[] = [
{
type: 'paragraph',
Expand Down Expand Up @@ -47,7 +49,7 @@ const Client: React.FC<ClientProps> = ({ id, name, slug, removeUser }) => {
)

const editor = useMemo(() => {
const slateEditor = withReact(withHistory(createEditor()))
const slateEditor = withLinks(withReact(withHistory(createEditor())))

const origin =
process.env.NODE_ENV === 'production'
Expand Down
7 changes: 7 additions & 0 deletions packages/example/src/Components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,11 @@ export const ClientFrame = styled.div`
color: #aaa;
font-style: italic;
}
a {
color: purple;
text-decoration: none;
}
a:visited {
color: darkmagenta;
}
`
82 changes: 38 additions & 44 deletions packages/example/src/EditorFrame.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useCallback } from 'react'

import { Transforms, Editor, Node } from 'slate'
import { Node } from 'slate'

import {
Slate,
ReactEditor,
Expand All @@ -13,7 +14,9 @@ import { ClientFrame, IconButton, Icon } from './Components'

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 {
editor: ReactEditor
Expand Down Expand Up @@ -51,11 +54,15 @@ const EditorFrame: React.FC<EditorFrame> = ({
<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" />

<LinkButton />
</div>

<Editable
Expand All @@ -70,50 +77,14 @@ const EditorFrame: React.FC<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 }) => {
switch (element.type) {
case 'link':
return (
<a {...attributes} href={element.href}>
{children}
</a>
)
case 'block-quote':
return <blockquote {...attributes}>{children}</blockquote>
case 'bulleted-list':
Expand Down Expand Up @@ -193,3 +164,26 @@ const MarkButton: React.FC<any> = ({ format, icon }) => {
</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 changes: 30 additions & 0 deletions packages/example/src/plugins/block.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 73 additions & 0 deletions packages/example/src/plugins/link.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 16 additions & 0 deletions packages/example/src/plugins/mark.ts
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit eb370ea

Please sign in to comment.