本文是ProseMirror开发实践系列的第二篇文章,主要是通过基于ProseMirror提供的一个示例包,来快速开发一个基本的富文本编辑器示例,让大家开始动手实践起来。
开始动手实践
ProseMirror本身是无头的富文本编辑器框架,它并没有提供一个操作的菜单。但其提供了一个官方的基本示例,包含了操作菜单,以及基本的富文本内容的编辑功能。本文将带你一步步地将该示例搭建并运行起来。
搭建项目
使用vite快速创建项目
pnpm create vite
- 输入项目名称:pm-example
- 选择框架: React
- 选择变体:TypeScript + SWC
创建完成后,可以使用vscode打pm-example目录。
安装依赖
我们先安装基本的依赖,进入项目目录,运行:
pnpm i
接着安装ProseMirror的相关依赖:
pnpm i prosemirror-model prosemirror-state prosemirror-view prosemirror-schema-basic prosemirror-schema-list prosemirror-example-setup
安装ProseMirror的开发工具
pnpm i prosemirror-dev-tools
编写App.tsx
⭐划重点了
下面的熟悉ProseMirror开发的第一步,即熟悉ProseMirror入口文件,以及最核心的几个模块的基本的接口的使用。
- 引入ProseMirror的相关模块,包括ProseMirror提供的基本示例——"prosemirror-example-setup",以及ProseMirror提供的开发工具——"prosemirror-dev-tools"。
- 初始化Schema,使用基本的schema,并结合列表的schema,来初始化Schema对象。
- 初始化EditorView,使用Dom解析器将初始的富文本内容转成默认的doc数据,并传入EditorState中,来初始化EditorView。
- 启用ProseMirror的开发工具。
import { useEffect, useRef } from "react";
import { EditorState } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import { Schema, DOMParser } from "prosemirror-model";
import { schema } from "prosemirror-schema-basic";
import { addListNodes } from "prosemirror-schema-list";
import { exampleSetup } from "prosemirror-example-setup";
import applyDevTools from "prosemirror-dev-tools";
import "./App.css";
function App() {
const editorRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
if (!editorRef.current) {
return;
}
// 将列表的相关文档模式定义也加入到基础的文档模式定义中去
const mySchema = new Schema({
nodes: addListNodes(schema.spec.nodes, "paragraph block*", "block"),
marks: schema.spec.marks,
});
const view = new EditorView(editorRef.current, {
state: EditorState.create({
doc: DOMParser.fromSchema(mySchema).parse(
document.querySelector("#content") as Node
),
plugins: exampleSetup({ schema: mySchema }),
}),
});
applyDevTools(view);
return () => {
view.destroy();
};
}, [editorRef]);
return <div className="editor" ref={editorRef}></div>;
}
export default App;
编写App.css
添加ProseMirror的菜单等相关样式
.ProseMirror {
position: relative;
}
.ProseMirror {
word-wrap: break-word;
white-space: pre-wrap;
white-space: break-spaces;
-webkit-font-variant-ligatures: none;
font-variant-ligatures: none;
font-feature-settings: "liga" 0; /* the above doesn't seem to work in Edge */
}
.ProseMirror pre {
white-space: pre-wrap;
}
.ProseMirror li {
position: relative;
}
.ProseMirror-hideselection *::selection {
background: transparent;
}
.ProseMirror-hideselection *::-moz-selection {
background: transparent;
}
.ProseMirror-hideselection {
caret-color: transparent;
}
/* See https://github.com/ProseMirror/prosemirror/issues/1421#issuecomment-1759320191 */
.ProseMirror [draggable][contenteditable="false"] {
user-select: text;
}
.ProseMirror-selectednode {
outline: 2px solid #8cf;
}
/* Make sure li selections wrap around markers */
li.ProseMirror-selectednode {
outline: none;
}
li.ProseMirror-selectednode:after {
content: "";
position: absolute;
left: -32px;
right: -2px;
top: -2px;
bottom: -2px;
border: 2px solid #8cf;
pointer-events: none;
}
/* Protect against generic img rules */
img.ProseMirror-separator {
display: inline !important;
border: none !important;
margin: 0 !important;
}
.ProseMirror-textblock-dropdown {
min-width: 3em;
}
.ProseMirror-menu {
margin: 0 -4px;
line-height: 1;
}
.ProseMirror-tooltip .ProseMirror-menu {
width: -webkit-fit-content;
width: fit-content;
white-space: pre;
}
.ProseMirror-menuitem {
margin-right: 3px;
display: inline-block;
}
.ProseMirror-menuseparator {
border-right: 1px solid #ddd;
margin-right: 3px;
}
.ProseMirror-menu-dropdown,
.ProseMirror-menu-dropdown-menu {
font-size: 90%;
white-space: nowrap;
}
.ProseMirror-menu-dropdown {
vertical-align: 1px;
cursor: pointer;
position: relative;
padding-right: 15px;
}
.ProseMirror-menu-dropdown-wrap {
padding: 1px 0 1px 4px;
display: inline-block;
position: relative;
}
.ProseMirror-menu-dropdown:after {
content: "";
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 4px solid currentColor;
opacity: 0.6;
position: absolute;
right: 4px;
top: calc(50% - 2px);
}
.ProseMirror-menu-dropdown-menu,
.ProseMirror-menu-submenu {
position: absolute;
background: white;
color: #666;
border: 1px solid #aaa;
padding: 2px;
}
.ProseMirror-menu-dropdown-menu {
z-index: 15;
min-width: 6em;
}
.ProseMirror-menu-dropdown-item {
cursor: pointer;
padding: 2px 8px 2px 4px;
}
.ProseMirror-menu-dropdown-item:hover {
background: #f2f2f2;
}
.ProseMirror-menu-submenu-wrap {
position: relative;
margin-right: -4px;
}
.ProseMirror-menu-submenu-label:after {
content: "";
border-top: 4px solid transparent;
border-bottom: 4px solid transparent;
border-left: 4px solid currentColor;
opacity: 0.6;
position: absolute;
right: 4px;
top: calc(50% - 4px);
}
.ProseMirror-menu-submenu {
display: none;
min-width: 4em;
left: 100%;
top: -3px;
}
.ProseMirror-menu-active {
background: #eee;
border-radius: 4px;
}
.ProseMirror-menu-disabled {
opacity: 0.3;
}
.ProseMirror-menu-submenu-wrap:hover .ProseMirror-menu-submenu,
.ProseMirror-menu-submenu-wrap-active .ProseMirror-menu-submenu {
display: block;
}
.ProseMirror-menubar {
border-top-left-radius: inherit;
border-top-right-radius: inherit;
position: relative;
min-height: 1em;
color: #666;
padding: 1px 6px;
top: 0;
left: 0;
right: 0;
border-bottom: 1px solid silver;
background: white;
z-index: 10;
-moz-box-sizing: border-box;
box-sizing: border-box;
overflow: visible;
}
.ProseMirror-icon {
display: inline-block;
line-height: 0.8;
vertical-align: -2px; /* Compensate for padding */
padding: 2px 8px;
cursor: pointer;
}
.ProseMirror-menu-disabled.ProseMirror-icon {
cursor: default;
}
.ProseMirror-icon svg {
fill: currentColor;
height: 1em;
}
.ProseMirror-icon span {
vertical-align: text-top;
}
.ProseMirror-gapcursor {
display: none;
pointer-events: none;
position: absolute;
}
.ProseMirror-gapcursor:after {
content: "";
display: block;
position: absolute;
top: -2px;
width: 20px;
border-top: 1px solid black;
animation: ProseMirror-cursor-blink 1.1s steps(2, start) infinite;
}
@keyframes ProseMirror-cursor-blink {
to {
visibility: hidden;
}
}
.ProseMirror-focused .ProseMirror-gapcursor {
display: block;
}
/* Add space around the hr to make clicking it easier */
.ProseMirror-example-setup-style hr {
padding: 2px 10px;
border: none;
margin: 1em 0;
}
.ProseMirror-example-setup-style hr:after {
content: "";
display: block;
height: 1px;
background-color: silver;
line-height: 2px;
}
.ProseMirror ul,
.ProseMirror ol {
padding-left: 30px;
}
.ProseMirror blockquote {
padding-left: 1em;
border-left: 3px solid #eee;
margin-left: 0;
margin-right: 0;
}
.ProseMirror-example-setup-style img {
cursor: default;
}
.ProseMirror-prompt {
background: white;
padding: 5px 10px 5px 15px;
border: 1px solid silver;
position: fixed;
border-radius: 3px;
z-index: 11;
box-shadow: -0.5px 2px 5px rgba(0, 0, 0, 0.2);
}
.ProseMirror-prompt h5 {
margin: 0;
font-weight: normal;
font-size: 100%;
color: #444;
}
.ProseMirror-prompt input[type="text"],
.ProseMirror-prompt textarea {
background: #eee;
border: none;
outline: none;
}
.ProseMirror-prompt input[type="text"] {
padding: 0 4px;
}
.ProseMirror-prompt-close {
position: absolute;
left: 2px;
top: 1px;
color: #666;
border: none;
background: transparent;
padding: 0;
}
.ProseMirror-prompt-close:after {
content: "✕";
font-size: 12px;
}
.ProseMirror-invalid {
background: #ffc;
border: 1px solid #cc7;
border-radius: 4px;
padding: 5px 10px;
position: absolute;
min-width: 10em;
}
.ProseMirror-prompt-buttons {
margin-top: 5px;
display: none;
}
#editor,
.editor {
background: white;
color: black;
background-clip: padding-box;
border-radius: 4px;
border: 2px solid rgba(0, 0, 0, 0.2);
padding: 5px 0;
margin-bottom: 23px;
}
.ProseMirror p:first-child,
.ProseMirror h1:first-child,
.ProseMirror h2:first-child,
.ProseMirror h3:first-child,
.ProseMirror h4:first-child,
.ProseMirror h5:first-child,
.ProseMirror h6:first-child {
margin-top: 10px;
}
.ProseMirror {
padding: 4px 8px 4px 14px;
line-height: 1.2;
outline: none;
}
.ProseMirror p {
margin-bottom: 1em;
}
修改index.html
添加加初始化的富文本内容
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ProseMirror基本示例</title>
</head>
<body>
<div id="root"></div>
<div style="display: none" id="content">
<h3>你好 ProseMirror</h3>
<p>
这是一个可以编辑的文本,你可以用鼠标聚焦到这里,然后开始使用键盘打字输入。
</p>
<p>
要修改样式,先选中文本,然后点击上面的菜单来修改它的样式。
基本的文档模式支持 <em>强调字体</em>, <strong>加粗文字</strong>,
<a href="http://marijnhaverbeke.nl/blog">链接</a>,
<code>代码字体</code>,和 <img src="/vite.svg" /> 图片
</p>
<p>
块级的结构可以使用快捷键进行操作,你可以尝试(ctrl-shift-2)来创建二级标题,
或者在空文本块中输入以退出父块)或通过菜单进行操作。
</p>
<p>你也可以试试使用菜单中的“列表”项将这个段落包裹成一个编号列表。</p>
</div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
修改index.css
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
}
改完收工
使用命令运行:
pnpm run dev
然后在浏览器中打开链接:http://localhost:5173
效果
打开ProseMirror dev工具后的效果
一个好的开始
该示例相对比较简单,因为这仅仅只是一个开始,这时我们使用ProseMirror进行富文本编辑器开发的一个开始。在这里示例里面,我们知道了一个最基本的ProseMirror示例需要用到哪些模块,以及如何去调用这些模块的接口,来初始化一个富文本编辑器。当你看到一个富文本编辑器出现在浏览器界面上时,祝贺你,迈出了第一步,进入了ProseMirror的世界。