什么是Draft.js
官网地址:draftjs.org/ 建议先看一遍官网文档再来阅读本文章
Draft.js是应用在react里的富文本编辑器框架,它主要优点如下
基础概念
EditeState分多层结构组织,大致的结构如下,下文会逐一介绍关键的概念
EditeState
EditeState是Draft富文本编辑器的顶层对象,它主要包含了编辑器中的内容、光标选中以及Entity&inline style等元信息
ContentState
是描述富文本编辑内容状态的对象,你可以认为你打字输入文本内容都是由这个对象下管理控制的。它会分有多个ContentBlock(为简化描述,后文都称作block),多个block实际上又是CharacterMetaData组成的,每一个CharacterMetaData会包含实际的字符以及他的Entity和inline style等信息
block是怎么划分的呢?
block可以想象是html的块级元素,列表、段落、代码块都看作是一个block
SelectionState
一个富文本编辑器,它除了内容状态的描述,它应该还会有光标状态的描述。这个状态可以判断光标是否重合、光标选中的锚点或焦点、选中的起始或终点的偏移量等
Entity
实体对象,它是一个数据结构,包含了一些元数据用来描述一段文本
用它可以实现链接插入、引用跳转等功能
Demo实践
Demo1
实现一个简单的文本编辑器,可以通过操作按钮、快捷键、markdown语法三种方式实现文本的加粗显示
操作按钮是类似于顶部功能菜单,快捷键通过command + b的方式加粗,特定语法是通过x加空格的方式加粗 源码地址:github.com/YYYYYHHHHHH…
环境准备
- 搭建react应用环境
// 安装脚手架
npm install -g create-react-app
// 创建app
npx create-react-app draft-demo
cd draft-demo
npm install
// 查看效果
npm run start

应用成功启动
关于
create-react-app的更多使用,可以参考官方文档create-react-app.dev/docs/gettin…
- 安装draft.js
npm install draft-js
功能菜单实现加粗
交互设计:用户使用光标选中文本后,可以点击功能按钮实现选中文本的加粗
- 首先我们要搭建编辑器的基础样式,为编辑器上方添加一排功能菜单,其中引入加粗功能按钮
import { useState } from "react";
import { Editor, EditorState } from "draft-js";
import "draft-js/dist/Draft.css";
import "./App.css";
function App() {
const [editorState, setEditorState] = useState(() =>
EditorState.createEmpty()
);
return (
<div className="editor-body">
<div className="tools">
<div
className='tool'
>
B
</div>
</div>
<div className="editor-wrap">
<Editor
placeholder="请输入..."
editorState={editorState}
onChange={setEditorState}
/>
</div>
</div>
);
}
export default App;
.editor-body {
width: 375px;
height: 200px;
border: 1px solid rgba(38, 38, 38, 0.5);
border-radius: 4px;
margin: 32px auto;
.tools {
padding: 4px 4px;
box-sizing: border-box;
border-bottom: 1px solid rgba(38, 38, 38, 0.5);
display: flex;
.tool {
color: #585a5a;
font-size: 14px;
padding: 4px 8px;
border: 4px;
font-weight: 500;
cursor: pointer;
}
}
.editor-wrap {
padding: 8px;
}
}
查看运行效果
2. 我们为加粗功能按钮添加点击事件,实现文案加粗
// ...
function App() {
// ...
const doBold = () => {
const selection = editorState.getSelection();
if (!selection.isCollapsed()) {
const newEditorState = RichUtils.toggleInlineStyle(editorState, 'BOLD');
setEditorState(newEditorState);
}
}
// 为加粗按钮添加点击事件
const onBoldClick = () => {
doBold();
}
return (
<div className="editor-body">
<div className="tools">
<div
className='tool'
onClick={onBoldClick}
>
B
</div>
</div>
...
</div>
);
}
我们使用了draft.js内置的RichUtils去实现了文本的加粗
运行效果
快捷键实现加粗
我们要实现command + b实现文案加粗,需要利用到自定义指令和指令的处理
// ...
function App() {
// ...
// 自定义快捷键函数
const keyBindingFn = (e) => {
if (e.key === 'b' && KeyBindingUtil.hasCommandModifier(e)) {
return 'my-bold'
}
// draft内置了许多快捷键处理,如undo、redo
return getDefaultKeyBinding(e);
}
// 自定义指令处理
const handleKeyCommand = (command) => {
if (command === 'my-bold') {
doBold();
return 'handled'
}
return 'not-handled'
};
return (
<div className="editor-body">
...
<div className="editor-wrap">
<Editor
placeholder="请输入..."
editorState={editorState}
onChange={setEditorState}
keyBindingFn={keyBindingFn}
handleKeyCommand={handleKeyCommand}
/>
</div>
</div>
);
}
export default App;
其中KeyBindingUtil是draft.js提供的内置类,他提供的hasCommandModifier是用来判断当前是否按下了快捷键的命令组合键(mac中的command,win中ctrl)getDefaultKeyBinding则是内置提供的默认快捷键命令处理,内置了undo、redo等功能(其实也是内置了加粗字体功能,实现加粗只不过是为了熟悉其功能)
markdown语法实现加粗
实现效果:
实现思路:
我们是需要捕获输入时的空格,然后需要寻找成对的**,去做替换,所以我们需要用到handleBeforeInput,在字符输入到富文本前,做操作拦截
handleBeforeInput
官方文档 表明:在此方法返回handled时,就能够阻止默认事件,这对我们的代码实现很有用,想象一下如果我们在实现加粗功能时,不能够阻止默认事件会怎么样 —— 输入空格后,文字确实被加粗了,但空格也被输入到编辑器中,这是我们不希望看到的
代码实现
import { useEffect, useState } from "react";
import {
Editor,
EditorState,
RichUtils,
KeyBindingUtil,
getDefaultKeyBinding,
CompositeDecorator,
Modifier,
SelectionState,
} from "draft-js";
import "draft-js/dist/Draft.css";
import "./App.css";
const regex = /\*\*(.*?)\*\*/g;
function App() {
const [editorState, setEditorState] = useState(() =>
EditorState.createEmpty()
);
// ...
const handleBeforeInput = (e) => {
// 如果输入空格,需要检查markdown语法
if (e === " ") {
const selection = editorState.getSelection();
const contentState = editorState.getCurrentContent();
const block = contentState.getBlockForKey(selection.getStartKey());
const text = block.getText();
// 获取光标的起始位置
const startOffset = selection.getStartOffset();
// 匹配到关键字,开始向前寻找关键字
if (text.slice(startOffset - 2, startOffset) === "**") {
let end = startOffset;
let start = end - 3;
while (start >= 0) {
if (text.slice(start, start + 2) === "**") {
break;
} else {
start--;
}
}
// 没有匹配到关键字,走默认事件
if (start < 0) {
return 'not-handled'
}
// 文本替换,把 **xx** 替换成 xx
const newContentStateWithText = Modifier.replaceText(
contentState,
selection.merge({
anchorOffset: start,
focusOffset: end,
}),
regex.exec(text.slice(0, startOffset))?.[1],
);
// 文本加粗
const newContentStateWithBold = Modifier.applyInlineStyle(
newContentStateWithText,
selection.merge({
anchorOffset: start,
focusOffset: end - 4,
}),
"BOLD"
);
// 生成新的EditorState对象
const newEditorState = EditorState.push(
editorState,
newContentStateWithBold,
"insert-characters"
);
// 为编辑器设置新的状态,并且把光标移动到最后
setEditorState(EditorState.moveFocusToEnd(newEditorState));
}
return "handled";
}
};
return (
<div className="editor-body">
// ...
<div className="editor-wrap">
<Editor
// ...
handleBeforeInput={handleBeforeInput}
/>
</div>
</div>
);
}
export default App;
上述代码中,因为editorState.getSelection();拿到的值是基于Immutable封装的,所以我们需要调用merge去修改光标选中的位置。正如文档开头说到的,Draft.js是immutable的,因为SelectionState是可以直接调用Immutable的api**, **所以我们在官方文档中没看到修改数据的api(这一块踩坑踩了好久,当初一直好奇怎么没有修改值的api :)