官方文档传送门:Slate React Docs
Slate 介绍
Slate 是一个完全可定制的框架,用于构建富文本编辑器。 通过 Slate,你可以构建像 Medium、Dropbox Paper 或 Google Docs 那样丰富、直观的编辑器,这些编辑器正在成为网络应用程序的桌面工具,而你的代码库却不会陷入复杂的泥潭。 它之所以能做到这一点,是因为它的所有逻辑都是通过一系列插件实现的,因此你永远不会受到 “核心 “内容的限制。你可以把它想象成是在 React 基础上对 contenteditable 的可插拔实现。它的灵感来自 Draft.js、Prosemirror 和 Quill 等库。
安装 Slate React
终端中执行 yarn add slate slate-react
当然,使用 Slate React 必须得基于 React 的项目,React 项目搭建:项目脚手架 create-react-app
在 React 中构建你的第一个富文本编辑器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Import React dependencies.
import React, { useState } from 'react'
// Import the Slate editor factory.
import { createEditor } from 'slate'
// Import the Slate components and React plugin.
import { Slate, Editable, withReact } from 'slate-react'
const initialValue = [
{
type: 'paragraph',
children: [{ text: 'A line of text in a paragraph.' }],
},
]
const App = () => {
const [editor] = useState(() => withReact(createEditor()))
return (
// Add the editable component inside the context.
<Slate editor={editor} initialValue={initialValue}>
<Editable />
</Slate>
)
}
监听编辑器的变化
手动添加自定义监听器,仅在 editor 发生变化时执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const App = () => {
const [editor] = useState(() => withReact(createEditor()))
// When editor has changed, excucte this function: handleSlateChange.
const handleSlateChange = useCallback(() => {
// ...
}, [editor])
return (
// Add the editable component inside the context.
<Slate editor={editor}
initialValue={initialValue}
// Add a listener.
onChange={(value) => handleSlateChange(value)}
>
<Editable />
</Slate>
)
}
Element & Leaf 介绍
在 Slate 中,Element 是表示文档结构中块级元素的概念。Slate 使用树状结构来组织文档,其中包含两种基本节点类型:Leaf 和 Element。Element 通常用来表示文档中的块级元素,比如段落、标题、列表等。
一个 Element 节点可以包含多个 Leaf 节点,从而构成一个完整的块级文本元素。Element 节点可以具有不同的类型和属性,以便表示各种不同的块级结构。例如,一个段落 Element 可能包含多个 Leaf 节点,每个 Leaf 代表段落中的一小部分文本,而该段落 Element 可能具有一些样式属性,比如对齐方式或其他格式化信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ./Element.js
const Element = (props) => {
const {attributes, children, element} = props
// 通过 block 的 type 进行判断,动态渲染不同的元素、组件
switch (element.type) {
default:
return (
<span {...attributes}>
{children}
</span>
)
}
}
在 Slate 中,Leaf 是指文本节点的叶子节点。Slate 是一个用于构建富文本编辑器的框架,它使用树状结构来表示文档,而文档中的最小单元是 Leaf。
在 Slate 中,Leaf 包含文本内容以及与之相关的样式信息。它是文本节点的基本构建块,用于表示编辑器中的文本片段。每个 Leaf 都可以具有不同的样式,例如粗体、斜体、下划线等,这些样式可以通过 Leaf 的属性来定义。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// ./Leaf.js
const Leaf = ({ attributes, children, leaf }) => {
if (leaf.bold) {
children = <strong>{children}</strong>
}
if (leaf.sub) {
children = <sub>{children}</sub>
}
if (leaf.sup) {
children = <sup>{children}</sup>
}
if (leaf.italic) {
children = <em>{children}</em>
}
if (leaf.underline) {
children = <u>{children}</u>
}
return <span {...attributes}>{children}</span>
}
export default Leaf
通过 Leaf 实现字体切换的具体代码示例如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
// ./slateComponents.js
// 控制 Leaf 字体切换的功能组件
import { forwardRef } from 'react'
import { Editor, Element as SlateElement, Transforms } from 'slate'
import { useSlate } from 'slate-react'
const Button = forwardRef((
{
className,
active,
reversed,
...props
},
ref
) => (
<span {...props} ref={ref} className={className} />
))
const Menu = forwardRef((
{ className, ...props },
ref
) => (
<div {...props} data-test-id="menu" ref={ref}/>
))
export const Toolbar = forwardRef((
{ className, ...props },
ref
) => (
<Menu {...props} ref={ref}/>
))
const LIST_TYPES = ['numbered-list', 'bulleted-list']
const TEXT_ALIGN_TYPES = ['left', 'center', 'right', 'justify']
export const toggleMark = (editor, format) => {
const isActive = isMarkActive(editor, format)
if (isActive) {
Editor.removeMark(editor, format)
} else {
Editor.addMark(editor, format, true)
}
}
const isBlockActive = (editor, format, blockType = 'type') => {
const { selection } = editor
if (!selection) return false
const [match] = Array.from(
Editor.nodes(editor, {
at: Editor.unhangRange(editor, selection),
match: n =>
!Editor.isEditor(n) &&
SlateElement.isElement(n) &&
n[blockType] === format,
})
)
return !!match
}
const isMarkActive = (editor, format) => {
const marks = Editor.marks(editor)
return marks ? marks[format] === true : false
}
export const BlockButton = ({ format, name }) => {
const editor = useSlate()
return (
<Button
active={isBlockActive(
editor,
format,
TEXT_ALIGN_TYPES.includes(format) ? 'align' : 'type'
)}
onMouseDown={event => {
event.preventDefault()
toggleBlock(editor, format)
}}
>
{name}
</Button>
)
}
export const MarkButton = ({ className, format, insert }) => {
const editor = useSlate()
return (
<Button
className={className}
active={isMarkActive(editor, format)}
onClick={event => {
event.preventDefault()
insert && toggleMark(editor, format)
}}
>
</Button>
)
}
export const toggleBlock = (editor, format) => {
const isActive = isBlockActive(
editor,
format,
TEXT_ALIGN_TYPES.includes(format) ? 'align' : 'type'
)
const isList = LIST_TYPES.includes(format)
Transforms.unwrapNodes(editor, {
match: n =>
!Editor.isEditor(n) &&
SlateElement.isElement(n) &&
LIST_TYPES.includes(n.type) &&
!TEXT_ALIGN_TYPES.includes(format),
split: true,
})
let newProperties
if (TEXT_ALIGN_TYPES.includes(format)) {
newProperties = {
align: isActive ? undefined : format,
}
} else {
newProperties = {
type: isActive ? 'paragraph' : isList ? 'list-item' : format,
}
}
Transforms.setNodes(editor, newProperties)
if (!isActive && isList) {
const block = { type: format, children: [] }
Transforms.wrapNodes(editor, block)
}
}
通过 Transforms 添加/修改/删除 Slate Node
添加元素
官方文档示例:Transforms.insertNodes
Transforms.insertNodes(editor: Editor, nodes: Node | Node[], options?)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// ./App.js
const App = () => {
const handleClick = () => {
const block = {
// 这里可不填写 text,可通过 children 进行嵌套,可添加 type 类型
// 在 Element 组件中可获取这个 block 的所有键值对
text: 'A new string of text.',
}
const position = {
at: [0, 1], // 位置索引
}
// 如果不填写 position,则默认添加到当前光标所处位置
Transforms.insertNodes(editor, block, position)
}
return(
// Add the editable component inside the context.
<Slate editor={editor}
initialValue={initialValue}
// Add a listener.
onChange={(value) => handleSlateChange(value)}
>
<button onClick={handleClick}>Insert Node</button>
<Editable renderLeaf={renderLeaf}
renderElement={renderElement}
/>
</Slate>
)
}
修改元素
官方文档示例:Transforms.setNodes
Transforms.setNodes(editor: Editor, props: Partial<Node>, options?)
1
2
3
4
5
6
7
const blank = {
type: 'blank',
children: [],
options: { size, maxLength, value },
}
// Change element at current location
Transforms.setNodes(editor, blank)
删除元素
官方文档示例:Transforms.removeNodes
Transforms.removeNodes(editor: Editor, options?)
快速判断光标所处位置
编辑器的业务需求繁多,其中重要的一点就是需要判断光标实时所处的位置,进行不同编辑器操作按钮的权限控制、以及划词提示等等。如果富文本通过 Transforms.insertNodes 添加的自定义 Slate Node 过多,又形成嵌套模式,则很难进行快速判断
因此写了一个快速判断光标位置的函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// According to the current slate node,
// search for the parent node in the upper layer,
// and the depth of the search is specified.
export const findAncestorAtDepth = (editor, node, depth) => {
const [nodeEntry] = Editor.nodes(editor, {
at: [],
match: n => n === node
})
if (!nodeEntry) {
return null // Node not found in the editor.
}
const [_, path] = nodeEntry
if (path.length <= depth) {
return null // Depth is greater than the path length, no ancestor found.
}
const ancestorPath = path.slice(0, -depth)
return Node.get(editor, ancestorPath)
}
得到指定层级的父节点后,通过 type 可以判断节点类型而进行不同的操作
根据业务需求重写 editor 内置方法
遇到复杂的键盘事件,可以通过重写 editor 的某些方法(比如 deleteBackward)来简化逻辑代码
官方 withTables 示例:withTables