用ProseMirror编写一个简单的WYSIWYG编辑器

当Sports.ru需要自己的WYSIWYG编辑器时,我们决定基于ProseMirror库进行制作。该工具的主要功能之一是其模块化和定制的广泛可能性,因此借助它的帮助,您可以非常精细地将编辑器定制为任何项目。特别是,《纽约时报》《卫报已使用ProseMirror 在本文中,我们将讨论如何使用ProseMirror编写所见即所得的编辑器。

用ProseMirror编写一个简单的WYSIWYG编辑器

ProseMirror概述


ProseMirror的作者是Marijn Haverbeke,他在前端开发人员社区中广为人知,主要是受欢迎的书籍Eloquent Javascript的作者。在我们的工作开始之初(2018年秋季),除了作者提供的官方文档和教程外,没有关于使用此库的材料。作者提供的文档包包括几个部分,其中最有用的是ProseMirror指南(基本概念的描述)和参考手册(库规范)。以下是《 ProseMirror指南》中主要思想的摘要。

ProseMirror始终将文档的状态存储在其自己的数据结构中。并且已经在运行时通过此结构在终端用户与之交互的页面上生成了相应的DOM元素。此外,ProseMirror不仅存储当前状态(状态),还存储先前更改的历史记录,必要时可以将其回滚。状态的任何更改都应通过事务进行,而使用DOM树进行的常规操作将无法在此处直接进行。事务是描述逐步状态更改逻辑的抽象。他们工作的本质是让人想起在状态管理库中发送和执行动作,例如Redux和Vuex。

该库本身建立在独立的模块上,可以根据需要禁用或添加这些模块。我们认为几乎每个人都需要的主要模块清单:

  • prosemirror-model-一个文档模型,描述文档的所有组件,其属性以及可以对其执行的操作;
  • prosemirror-state-一种描述特定时间点创建的文档状态的数据结构,包括选定的片段和从一种状态转移到另一种状态的事务;
  • prosemirror-view-在浏览器和工具中呈现文档,以便用户可以与文档进行交互;
  • prosemirror-transform-用于存储事务历史记录的功能,借助该功能可实现事务,并允许您回滚到以前的状态或领导一个文档的联合开发。

除此最小设置外,以下模块也可能有用:


ProseMirror


让我们从创建编辑器大纲开始。 ProseMirror中的架构定义了文档中可能包含的元素及其属性的列表。每个元素都有一个toDOM方法,该方法确定如何在网页的DOM树中表示此元素。

所见即所得的原理正是在以下事实中实现的:创建方案时,我们可以完全控制页面上可编辑内容的显示方式,因此,我们可以为每个元素提供HTML结构并设置样式,就像要查看的内容一样。可以根据您的要求创建和自定义节点,尤其是很可能需要段落,标题,列表,图片,段落,媒体。

假设文本的每个段落都应使用类别为“ paragraph”的标签包裹在“ p”标签中,该标签确定布局所需的段落样式规则。然后,在这种情况下,ProseMirror模式可能如下所示:

import { Schema } from "prosemirror-model";

const mySchema = new Schema({
    nodes: {
        doc: {
           content: 'block'
        },
        text: {},
        paragraph: {
            content: 'inline',
            group: 'block',
            toDOM: function toDOM(node) {
                return ['p', {class: 'paragraph'}, 0];
            },
        },
    },
});

首先,我们导入构造函数以创建架构,并在其中传递一个对象,该对象在将来的编辑器中描述节点(以下称为节点)。节点是描述正在创建的内容类型的抽象。例如,使用这种方案,只有文本和段落类型的节点可以在编辑器中。 Doc是顶级节点的名称,该顶级节点将仅由块元素组成,即在这种情况下,仅来自段落(因为我们没有描述其他段落)。

文本是文本节点,有点类似于文本DOM节点。使用group属性,我们可以以不同的方式对节点进行分组,以便可以在代码中更轻松地访问它们。可以通过任何方便的方式设置组。在我们的案例中,我们仅将节点划分为block和inline。文本默认为内联,因此可以显式省略。

段落(段落)是完全不同的东西。我们宣布段落由内联文本元素组成,并且在DOM中也有自己的表示形式。大致从DOM块元素的意义上来说,一个段落将是一个块元素。根据此方案,页面上的段落将在在线编辑器中显示如下:

<p class="paragraph">Content of the paragraph</p>

现在,您可以创建编辑器本身:

import { EditorState } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import { Schema } from "prosemirror-model";

const mySchema = new Schema({
    nodes: {
        doc: {
            content: 'block+'
        },
        text: {
            group: 'inline',
            inline: true
        },
        paragraph: {
            content: 'inline*',
            group: 'block',
            toDOM: function toDOM(node) {
                return ['p', {class: 'paragraph'}, 0];
            },
        },
    },
});

/**
 * @classdesc  
 * @param {Object} el DOM-,    
 */
export default class Wysiwyg {
    constructor(el) {
        this.el = el;
    }

    /**
      * @description     
      * @param {Object} content ,    
      */
    setArticle(content) {
        const state = EditorState.fromJSON(
            {schema: mySchema},
            content,
        );
        const view = new EditorView(this.el, {state: state});
    }
}

首先,像往常一样,我们从相应的模块中导入所有必要的构造函数,并采用上述方案。我们将编辑器创建为带有一组必要方法的类。为了使网页能够编辑和创建内容,您需要使用文章的布局和内容创建状态,状态,并在给定的根元素中显示当前状态的内容。我们将这些操作放入setArticle方法中,但到目前为止,不需要任何操作。

同时,内容是可选的。如果不是,那么您将获得一个空的编辑器,并且内容可以直接在现场创建。假设我们有一个带有此标记的HTML文件:

<div id="editor"></div>

要在页面上创建一个空的WYSIWYG编辑器,只需要在带有此标记的页面上运行的脚本中几行即可:

//     
import Wysiwyg from 'wysiwyg';

const root = document.getElementById('editor');
const myWysiwyg = new Wysiwyg(root);
myWysiwyg.setArticle();

使用此代码,您可以从头开始编写任何文本。至此,您已经可以看到ProseMirror的工作原理。

当用户在此编辑器中输入文本时,将发生状态事务。例如,如果使用上述方案在编辑器中键入短语“ This is my new酷的所见即所得编辑器”,ProseMirror将通过调用适当的事务集来响应键盘输入,并且在输入完成后,文档状态中的内容将如下所示:

content: [
    {
        type: 'paragraph',
        value: '    WYSIWYG-',
    },
]

如果我们想在编辑器中打开一些文本(例如,早先创建的文章的内容)进行编辑,则该内容必须与早先创建的方案相对应。然后,编辑器的初始化代码看起来会有些不同:

//     
import Wysiwyg from 'wysiwyg';

const root = document.getElementById('editor');
const myWysiwyg = new Wysiwyg(root);
const content = {
    type: 'doc',
    content: [
        {
            type: 'paragraph',
            value: 'Hello, world!',
        },
        {
            type: 'paragraph',
            value: 'This is my first wysiwyg-editor',
        }
    ],
};
myWysiwyg.setArticle(content);

万岁!我们制作了第一个编辑器,它可以创建一个干净的页面来创建新内容并打开现有内容进行编辑。

接下来如何处理编辑器


但是即使有完整的节点集,我们的编辑器仍然缺少重要的功能-设置文本,段落的格式。为此,除节点外,还必须将具有格式设置,标记的对象传送到电路。为了简单起见,我们称其为-品牌。为了控制所有这些元素的添加,删除和格式化,用户将需要一个菜单​​。可以使用自定义插件添加菜单,这些插件可对菜单对象进行样式化,并在选择某些操作时描述文档状态的变化。

使用插件,您可以创建任何扩展编辑器内置功能的设计。插件的主要思想是它们处理某些用户操作以生成与其相对应的事务。例如,如果您想单击菜单中的列表图标以创建一个新的空列表并将光标移至第一个元素的开头,那么我们绝对需要在相应的插件中描述此事务。

您可以在官方文档中阅读有关格式设置和插件的更多信息,以及使用ProseMirror功能的很小的有用示例可能非常有用

UPD如果您对我们如何将基于ProseMirror的新编辑器集成到现有项目中感兴趣,那么我们在另一篇文章中对此进行了讨论

All Articles