Escrevendo um editor WYSIWYG simples com ProseMirror

Quando o Sports.ru precisou de seu próprio editor WYSIWYG , decidimos fazê-lo com base na biblioteca ProseMirror. Um dos principais recursos dessa ferramenta é sua modularidade e amplas possibilidades de personalização, portanto, com sua ajuda, você pode adaptar o editor de maneira muito precisa a qualquer projeto. Em particular, o ProseMirror já está sendo usado no The New York Times e no The Guardian . Neste artigo, falaremos sobre como escrever seu editor WYSIWYG usando o ProseMirror.

Escrevendo um editor WYSIWYG simples com ProseMirror

Visão geral do ProseMirror


O autor do ProseMirror é Marijn Haverbeke, conhecido na comunidade de desenvolvedores de front-end principalmente como o autor do popular livro Eloquent Javascript . No início de nosso trabalho (outono de 2018), não havia materiais sobre como trabalhar com esta biblioteca, exceto a documentação oficial e os tutoriais do autor. O pacote de documentação do autor inclui várias seções, sendo as mais úteis o Guia ProseMirror (descrição dos conceitos básicos) e o Manual de referência (especificação da biblioteca). Abaixo está um resumo das principais idéias do Guia ProseMirror.

O ProseMirror sempre armazena o estado de um documento em sua própria estrutura de dados. E já a partir dessa estrutura em tempo de execução, os elementos DOM correspondentes são gerados na página com a qual o usuário final interage. Além disso, o ProseMirror armazena não apenas o estado atual (estado), mas também o histórico de alterações anteriores, que podem ser revertidas, se necessário. Qualquer mudança de estado deve ocorrer por meio de transações; as manipulações usuais com a árvore DOM não funcionarão diretamente aqui. Uma transação é uma abstração que descreve a lógica de uma mudança de estado passo a passo. A essência de seu trabalho é uma reminiscência de envio e execução de ações em bibliotecas para gerenciamento de estado, por exemplo, Redux e Vuex.

A própria biblioteca é construída em módulos independentes que podem ser desabilitados ou adicionados dependendo das necessidades. Uma lista dos principais módulos que pensávamos serem necessários para quase todos:

  • prosemirror-model - um modelo de documento que descreve todos os componentes de um documento, suas propriedades e ações que podem ser executadas neles;
  • prosemirror-state - uma estrutura de dados que descreve o estado do documento criado em um determinado momento, incluindo fragmentos e transações selecionados para mover de um estado para outro;
  • prosemirror-view - apresentação do documento no navegador e ferramentas para que o usuário possa interagir com o documento;
  • prosemirror-transform - uma funcionalidade para armazenar o histórico de transações, com a ajuda de quais transações são implementadas e que permite reverter para estados anteriores ou conduzir o desenvolvimento conjunto de um documento.

Além deste conjunto mínimo, os seguintes módulos também podem ser úteis:

  • prosemirror-command - um conjunto de comandos prontos para edição. Como regra, você precisa escrever algo mais complexo ou individual, mas Marijn Haverbeke já fez algumas coisas por nós, por exemplo, excluindo um fragmento selecionado de texto;
  • prosemirror-keymap - um módulo de dois métodos para especificar atalhos de teclado;
  • prosemirror-history – , .. , , , ;
  • prosemirror-schema-list – , ( DOM-, , ).

ProseMirror


Vamos começar criando um esboço do editor. O esquema no ProseMirror define uma lista de elementos que podem estar em nosso documento e suas propriedades. Cada elemento possui um método toDOM, que determina como esse elemento será representado na árvore DOM na página da web.

O princípio do WYSIWYG é realizado precisamente no fato de que, ao criar o esquema, temos controle total sobre como o conteúdo editável é exibido na página e, portanto, podemos dar a cada elemento uma estrutura HTML e definir estilos exatamente como o conteúdo a ser exibido. Os nós podem ser criados e personalizados de acordo com seus requisitos, em particular, provavelmente você precisará de parágrafos, títulos, listas, imagens, parágrafos, mídia.

Suponha que cada parágrafo do texto seja envolto em uma tag "p" com a classe "paragraph", que determina as regras dos estilos de parágrafo necessários para o layout. Em seguida, o esquema ProseMirror nesse caso pode ser assim:

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];
            },
        },
    },
});

Primeiro, importamos o construtor para criar nosso esquema e passar um objeto para ele que descreve os nós (daqui em diante referidos como nós) em um futuro editor. Nós são abstrações que descrevem os tipos de conteúdo que estão sendo criados. Por exemplo, com esse esquema, apenas nós dos tipos texto e parágrafo podem estar no editor. Doc é o nome do nó de nível superior, que consiste apenas em elementos de bloco, ou seja, neste caso, apenas a partir de parágrafos (porque não descrevemos outros).

Texto são nós de texto, um pouco semelhantes aos nós do DOM de texto. Usando a propriedade group, podemos agrupar nossos nós de maneiras diferentes, para que possam ser acessados ​​com mais facilidade no código. Os grupos podem ser definidos de qualquer maneira conveniente para nós. No nosso caso, dividimos os nós apenas em bloco e em linha. O texto está embutido por padrão, portanto, isso pode ser omitido explicitamente.

Uma coisa completamente diferente é o parágrafo (parágrafo). Anunciamos que os parágrafos consistem em elementos de texto embutido e também têm sua própria representação no DOM. Um parágrafo será um elemento de bloco, aproximadamente no sentido de que são os elementos de bloco DOM. De acordo com esse esquema, os parágrafos da página serão apresentados no editor on-line da seguinte maneira:

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

Agora você pode criar o próprio editor:

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});
    }
}

No início, como sempre, importamos todos os construtores necessários dos módulos correspondentes e adotamos o esquema descrito acima. Criamos o editor como uma classe com um conjunto de métodos necessários. Para que a página da web possa editar e criar conteúdo, é necessário criar um estado, estado, usando o layout e o conteúdo do artigo, e apresentando o conteúdo do estado atual no elemento raiz especificado. Colocamos essas ações no método setArticle, exceto pelas quais nada é necessário até o momento.

Ao mesmo tempo, o conteúdo é opcional. Caso contrário, você receberá um editor vazio e o conteúdo já poderá ser criado diretamente no local. Digamos que temos um arquivo HTML com esta marcação:

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

Para criar um editor WYSIWYG vazio em uma página, você precisa de apenas algumas linhas em um script que é executado em uma página com esta marcação:

//     
import Wysiwyg from 'wysiwyg';

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

Usando esse código, você pode escrever qualquer texto do zero. Neste ponto, você já pode ver como o ProseMirror funciona.

Quando um usuário digita texto neste editor, ocorrem transações de estado. Por exemplo, se você digitar a frase “Este é meu novo editor WYSIWYG legal” no editor com o esquema acima, o ProseMirror responderá à entrada do teclado invocando o conjunto apropriado de transações e, depois que a entrada for concluída, o conteúdo no estado do documento será semelhante a este:

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

Se quisermos que algum texto, por exemplo, o conteúdo de um artigo já criado anteriormente, seja aberto no editor para edição, esse conteúdo deverá corresponder ao esquema criado anteriormente. Em seguida, o código de inicialização do editor será um pouco diferente:

//     
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);

Viva! Criamos nosso primeiro editor, que pode criar uma página limpa para criar novo conteúdo e abrir os existentes para edição.

O que fazer com o editor a seguir


Mas mesmo com um conjunto completo de nós, nosso editor ainda não possui funções importantes - formatação de texto, parágrafos. Para isso, além dos nós, um objeto com configurações de formatação, marcas também deve ser transferido para o circuito. Por simplicidade, chamamos-lhes assim - marcas. E para controlar a adição, exclusão e formatação de todos esses elementos, os usuários precisarão de um menu. Os menus podem ser adicionados usando plug-ins personalizados que estilizam os objetos de menu e descrevem a alteração no estado de um documento ao escolher determinadas ações.

Usando plug-ins, você pode criar qualquer design que amplie os recursos internos do editor. A idéia principal dos plug-ins é que eles processem determinadas ações do usuário para gerar transações correspondentes a eles. Por exemplo, se você deseja clicar no ícone da lista no menu para criar uma nova lista vazia e mover o cursor para o início do primeiro elemento, definitivamente precisamos descrever essa transação no plug-in correspondente.

Você pode ler mais sobre configurações de formatação e plug-ins na documentação oficial , bem como pequenos exemplos úteis do uso dos recursos do ProseMirror podem ser muito úteis .

UPDSe você estiver interessado em como integramos um novo editor baseado no ProseMirror a um projeto existente, falamos sobre isso em outro artigo .

All Articles