ProseMirror基本示例开发

80 阅读6分钟

本文是ProseMirror开发实践系列的第二篇文章,主要是通过基于ProseMirror提供的一个示例包,来快速开发一个基本的富文本编辑器示例,让大家开始动手实践起来。

开始动手实践

ProseMirror本身是无头的富文本编辑器框架,它并没有提供一个操作的菜单。但其提供了一个官方的基本示例,包含了操作菜单,以及基本的富文本内容的编辑功能。本文将带你一步步地将该示例搭建并运行起来。

搭建项目

使用vite快速创建项目

pnpm create vite
  1. 输入项目名称:pm-example
  2. 选择框架: React
  3. 选择变体: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入口文件,以及最核心的几个模块的基本的接口的使用。

  1. 引入ProseMirror的相关模块,包括ProseMirror提供的基本示例——"prosemirror-example-setup",以及ProseMirror提供的开发工具——"prosemirror-dev-tools"。
  2. 初始化Schema,使用基本的schema,并结合列表的schema,来初始化Schema对象。
  3. 初始化EditorView,使用Dom解析器将初始的富文本内容转成默认的doc数据,并传入EditorState中,来初始化EditorView。
  4. 启用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的世界。