使用 tiptap 实现简易富文本编辑器


使用 tiptap 实现简易富文本编辑器

前言

在工作中遇到实现富文本编辑器的功能,最后是使用了 tiptap 来实现,记录一下。

简单使用

直接看官网的快速开始教程即可,安装@tiptap/react@tiptap/pm@tiptap/starter-kit这三剑客。

使用方法也很简单,使用useEditor Hook 得到 Editor 对象,然后赋值给EditorContent的 editor 属性即可

EditorX.tsx

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
import { EditorContent, useEditor } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";

import "./index.css";

const extensions = [StarterKit];

type EditorXProps = {
defaultValue?: string;
onChange?: (val?: string) => void;
};

const EditorX = (props: EditorXProps) => {
const { defaultValue: content, onChange } = props;

const editor = useEditor({
extensions: [...extensions],
content,
onUpdate({ editor }) {
if (!editor.getText()) {
onChange?.(undefined);
} else {
onChange?.(editor.getText());
}
},
});

return (
<>
<EditorContent editor={editor} />
</>
);
};

export default EditorX;

index.css

1
2
3
4
5
6
7
8
9
10
11
12
.tiptap.ProseMirror {
min-height: 62px;
outline: none;
}

.tiptap p.is-editor-empty:first-child::before {
color: #adb5bd;
content: attr(data-placeholder);
float: left;
height: 0;
pointer-events: none;
}

app.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { useState } from "react";
import EditorX from "./components/EditorX";

function App() {
const [outerValue, setOuterValue] = useState("clz");

const handleChange = (value?: string) => {
if (value) {
setOuterValue(value);
}
};

return (
<>
{outerValue}
<EditorX defaultValue="clz" onChange={handleChange} />
</>
);
}

export default App;

使用插件@tiptap/extension-placeholder

tiptap 编辑器并不是 input 这种输入框,而是通过普通的 div 元素,只是通过contenteditable这个属性来让 dom 元素可以编辑来实现的。所以自然不能直接使用 placeholder。tiptap 使用placeholder需要安装插件。

插件使用:

  1. 引入

    1
    import Placeholder from "@tiptap/extension-placeholder";
  2. extensions 添加插件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const editor = useEditor({
    extensions: [
    ...extensions,
    Placeholder.configure({
    placeholder: "赤蓝紫到此一游",
    }),
    ],
    // ...
    });

添加操作菜单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const EditorX = () => {
// ...
const handleBold = () => {
editor?.commands.toggleBold();
};

return (
<>
<EditorContent editor={editor} />

{editor && (
<BubbleMenu editor={editor}>
<span className="menu-bold" onClick={handleBold}>
B
</span>
</BubbleMenu>
)}
</>
);

// ...
};

样式

1
2
3
4
5
6
7
8
9
10
.menu-bold {
display: block;
padding: 0 12px;
font-weight: bold;
cursor: pointer;
}

.menu-bold:hover {
background-color: #eee;
}

通过BubbleMenu展示操作菜单,并利用editor?.commands提供的 api 进行 tiptap 内置的一些操作,如上面的加粗。

粘贴图片

useEditor参数对象可以接收editorProps属性,而editorProps属性里面有一个handlePaste方法,该方法的第二个方法就可以获取粘贴的文件信息,然后再利用FileReader就可以把文件以base64码的形式读取。

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
const handlePaste = async (view: EditorView, event: ClipboardEvent) => {
const files = event.clipboardData?.files;
const file = files?.item(0);

if (!file || !file.type.startsWith("image/")) {
return;
}

const reader = new FileReader();
reader.readAsDataURL(file);

reader.onload = () => {
if (reader.result) {
console.log(reader.result);
}
};
};

const editor = useEditor({
extensions: [
...extensions,
Placeholder.configure({
placeholder: "赤蓝紫到此一游",
}),
Image.configure({
inline: true,
}),
],
content,
onUpdate({ editor }) {
console.log(editor);

if (!editor.getText()) {
onChange?.(undefined);
} else {
onChange?.(editor.getText());
}
},
editorProps: {
handlePaste(view, event) {
handlePaste(view, event);
},
},
});

然后通过添加@tiptap/extension-image插件把图片添加到编辑器上即可。

$\color{red}{需要注意的是,需要配置inlinetrue,否则会出现图片粘贴上去,立马就被移出DOM树}$

1
2
3
4
5
6
7
8
9
10
11
reader.onload = () => {
if (reader.result) {
editorRef.current
?.chain()
.focus()
.setImage({
src: reader.result as string,
})
.run();
}
};

完整代码

EditorX.tsx

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
import { useRef } from "react";
import { BubbleMenu, Editor, EditorContent, useEditor } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import Placeholder from "@tiptap/extension-placeholder";
import Image from "@tiptap/extension-image";

import { EditorView } from "@tiptap/pm/view";
import "./index.css";

const extensions = [
StarterKit,
Placeholder.configure({
placeholder: "赤蓝紫到此一游",
}),
Image.configure({
inline: true,
}),
];

type EditorXProps = {
defaultValue?: string;
onChange?: (val?: string) => void;
};

const EditorX = (props: EditorXProps) => {
const { defaultValue: content, onChange } = props;

const editorRef = useRef<Editor | null>(null);
const handlePaste = async (view: EditorView, event: ClipboardEvent) => {
const files = event.clipboardData?.files;
const file = files?.item(0);

if (!file || !file.type.startsWith("image/")) {
return;
}

const reader = new FileReader();
reader.readAsDataURL(file);

reader.onload = () => {
if (reader.result) {
editorRef.current
?.chain()
.focus()
.setImage({
src: reader.result as string,
})
.run();
}
};
};

const editor = useEditor({
extensions: extensions,
content,
onUpdate({ editor }) {
if (!editor.getText()) {
onChange?.(undefined);
} else {
onChange?.(editor.getText());
}
},
editorProps: {
handlePaste(view, event) {
handlePaste(view, event);
},
},
});
editorRef.current = editor;

const handleBold = () => {
editor?.commands.toggleBold();
};

return (
<>
<EditorContent editor={editor} />

{editor && (
<BubbleMenu editor={editor}>
<span className="menu-bold" onClick={handleBold}>
B
</span>
</BubbleMenu>
)}
</>
);
};

export default EditorX;

index.css

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.tiptap.ProseMirror {
min-height: 62px;
outline: none;
}

.tiptap p.is-editor-empty:first-child::before {
color: #adb5bd;
content: attr(data-placeholder);
float: left;
height: 0;
pointer-events: none;
}

.menu-bold {
display: block;
padding: 0 12px;
font-weight: bold;
cursor: pointer;
}

.menu-bold:hover {
background-color: #eee;
}

文章作者: 赤蓝紫
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 赤蓝紫 !
评论
  目录