diff --git a/packages/backend/package.json b/packages/backend/package.json index 959846e..cdcdb2a 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -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" }, diff --git a/packages/bridge/package.json b/packages/bridge/package.json index 643e00e..b7f16fe 100644 --- a/packages/bridge/package.json +++ b/packages/bridge/package.json @@ -26,7 +26,7 @@ }, "dependencies": { "automerge": "0.14.0", - "slate": "0.58.0", + "slate": "0.58.3", "typescript": "^3.8.3" }, "devDependencies": { diff --git a/packages/client/package.json b/packages/client/package.json index 288d4c4..91e0d4e 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -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" }, diff --git a/packages/example/package.json b/packages/example/package.json index fa43a35..f1e0016 100644 --- a/packages/example/package.json +++ b/packages/example/package.json @@ -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", @@ -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": { diff --git a/packages/example/src/Client.tsx b/packages/example/src/Client.tsx index b2d5fbf..ade9614 100644 --- a/packages/example/src/Client.tsx +++ b/packages/example/src/Client.tsx @@ -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', @@ -47,7 +49,7 @@ const Client: React.FC = ({ 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' diff --git a/packages/example/src/Components.tsx b/packages/example/src/Components.tsx index 43e12d4..38be5b7 100644 --- a/packages/example/src/Components.tsx +++ b/packages/example/src/Components.tsx @@ -83,4 +83,11 @@ export const ClientFrame = styled.div` color: #aaa; font-style: italic; } + a { + color: purple; + text-decoration: none; + } + a:visited { + color: darkmagenta; + } ` diff --git a/packages/example/src/EditorFrame.tsx b/packages/example/src/EditorFrame.tsx index cc867e5..2db5d67 100644 --- a/packages/example/src/EditorFrame.tsx +++ b/packages/example/src/EditorFrame.tsx @@ -1,6 +1,7 @@ import React, { useCallback } from 'react' -import { Transforms, Editor, Node } from 'slate' +import { Node } from 'slate' + import { Slate, ReactEditor, @@ -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 @@ -51,11 +54,15 @@ const EditorFrame: React.FC = ({ + + + + = ({ 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 = ({ attributes, children, element }) => { switch (element.type) { + case 'link': + return ( + + {children} + + ) case 'block-quote': return
{children}
case 'bulleted-list': @@ -193,3 +164,26 @@ const MarkButton: React.FC = ({ format, icon }) => { ) } + +const LinkButton = () => { + const editor = useSlate() + + const isActive = isLinkActive(editor) + + return ( + { + event.preventDefault() + + if (isActive) return unwrapLink(editor) + + const url = window.prompt('Enter the URL of the link:') + + url && insertLink(editor, url) + }} + > + link + + ) +} diff --git a/packages/example/src/plugins/block.ts b/packages/example/src/plugins/block.ts new file mode 100644 index 0000000..5edeea3 --- /dev/null +++ b/packages/example/src/plugins/block.ts @@ -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 +} diff --git a/packages/example/src/plugins/link.ts b/packages/example/src/plugins/link.ts new file mode 100644 index 0000000..eef086f --- /dev/null +++ b/packages/example/src/plugins/link.ts @@ -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 = (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' }) + } +} diff --git a/packages/example/src/plugins/mark.ts b/packages/example/src/plugins/mark.ts new file mode 100644 index 0000000..453b25f --- /dev/null +++ b/packages/example/src/plugins/mark.ts @@ -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 +}