Escribir un editor WYSIWYG simple con ProseMirror

Cuando Sports.ru necesitaba su propio editor WYSIWYG , decidimos hacerlo basado en la biblioteca ProseMirror. Una de las características clave de esta herramienta es su modularidad y amplias posibilidades de personalización, por lo que con su ayuda puede adaptar el editor a cualquier proyecto. En particular, ProseMirror ya se está utilizando en The New York Times y The Guardian . En este artículo, hablaremos sobre cómo escribir su editor WYSIWYG usando ProseMirror.

Escribir un editor WYSIWYG simple con ProseMirror

Descripción general de ProseMirror


La autora de ProseMirror es Marijn Haverbeke, conocida en la comunidad de desarrolladores frontend principalmente como la autora del popular libro Eloquent Javascript . Al comienzo de nuestro trabajo (otoño de 2018), no había materiales para trabajar con esta biblioteca, excepto documentación oficial y tutoriales del autor. El paquete de documentación del autor incluye varias secciones, las más útiles son la Guía ProseMirror (descripción de conceptos básicos) y el Manual de referencia (especificación de la biblioteca). A continuación se muestra un resumen de las ideas clave de la Guía ProseMirror.

ProseMirror siempre almacena el estado de un documento en su propia estructura de datos. Y ya desde esta estructura en tiempo de ejecución, los elementos DOM correspondientes se generan en la página con la que interactúa el usuario final. Además, ProseMirror almacena no solo el estado actual (estado), sino también el historial de cambios anteriores, que pueden revertirse si es necesario. Cualquier cambio en el estado debe ocurrir a través de transacciones, las manipulaciones habituales con el árbol DOM no funcionarán directamente aquí. Una transacción es una abstracción que describe la lógica de un cambio de estado paso a paso. La esencia de su trabajo es una reminiscencia de enviar y ejecutar acciones en bibliotecas para la gestión del estado, por ejemplo, Redux y Vuex.

La biblioteca en sí está construida sobre módulos independientes que se pueden deshabilitar o agregar según las necesidades. Una lista de los módulos principales que pensamos que sería necesaria para casi todos:

  • modelo de prosemirror : un modelo de documento que describe todos los componentes de un documento, sus propiedades y acciones que se pueden realizar en ellos;
  • prosemirror-state : una estructura de datos que describe el estado del documento creado en un determinado momento, incluidos fragmentos y transacciones seleccionados para pasar de un estado a otro;
  • prosemirror-view : presentación del documento en el navegador y las herramientas para que el usuario pueda interactuar con el documento;
  • prosemirror-transform : una función para almacenar el historial de transacciones, con la ayuda de las transacciones que se implementan y que le permite retroceder a estados anteriores o realizar un desarrollo conjunto de un documento

Además de este conjunto mínimo, los siguientes módulos también pueden ser útiles:

  • comandos de prosemirror : un conjunto de comandos listos para editar. Como regla general, debe escribir algo más complejo o individual, pero Marijn Haverbeke ya ha hecho algunas cosas por nosotros, por ejemplo, eliminar un fragmento de texto seleccionado;
  • prosemirror-keymap : un módulo de dos métodos para especificar métodos abreviados de teclado;
  • prosemirror-history – , .. , , , ;
  • prosemirror-schema-list – , ( DOM-, , ).

ProseMirror


Comencemos creando un esquema del editor. El esquema en ProseMirror define una lista de elementos que pueden estar en nuestro documento y sus propiedades. Cada elemento tiene un método toDOM, que determina cómo se representará este elemento en el árbol DOM de la página web.

El principio de WYSIWYG se realiza precisamente en el hecho de que al crear el esquema tenemos control total sobre cómo se muestra el contenido editable en la página y, en consecuencia, podemos dar a cada elemento una estructura HTML y establecer estilos como el contenido que se va a ver. Los nodos se pueden crear y personalizar según sus requisitos, en particular, lo más probable es que necesite párrafos, encabezados, listas, imágenes, párrafos, medios.

Suponga que cada párrafo del texto debe estar envuelto en una etiqueta "p" con la clase "párrafo", que determina las reglas de los estilos de párrafo necesarios para el diseño. Entonces el esquema ProseMirror en este caso puede verse así:

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

Primero, importamos el constructor para crear nuestro esquema y pasarle un objeto que describa los nodos (en adelante denominados nodos) en el futuro editor. Los nodos son abstracciones que describen los tipos de contenido que se crean. Por ejemplo, con dicho esquema, solo los nodos de tipos de texto y párrafo pueden estar en el editor. Doc es el nombre del nodo de nivel superior, que consistirá solo en elementos de bloque, es decir en este caso solo de párrafos (porque no hemos descrito otros).

El texto son nodos de texto, algo similar a los nodos DOM de texto. Usando la propiedad group, podemos agrupar nuestros nodos de diferentes maneras para que se pueda acceder más fácilmente en código. Los grupos se pueden configurar de cualquier manera conveniente para nosotros. En nuestro caso, dividimos los nodos solo en bloque e en línea. El texto está en línea de forma predeterminada, por lo que se puede omitir explícitamente.

Una cosa completamente diferente es el párrafo (párrafo). Anunciamos que los párrafos consisten en elementos de texto en línea y también tienen su propia representación en el DOM. Un párrafo será un elemento de bloque, aproximadamente en el sentido de que lo son los elementos de bloque DOM. De acuerdo con este esquema, los párrafos de la página se presentarán en el editor en línea de la siguiente manera:

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

Ahora puede crear el editor en sí:

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

Al principio, como de costumbre, importamos todos los constructores necesarios de los módulos correspondientes y tomamos el esquema descrito anteriormente. Creamos el editor como una clase con un conjunto de métodos necesarios. Para que la página web pueda editar y crear contenido, debe crear un estado, utilizando el diseño y el contenido del artículo, y presentando el contenido del estado actual en el elemento raíz dado. Ponemos estas acciones en el método setArticle, excepto que hasta ahora no se necesita nada.

Al mismo tiempo, el contenido es opcional. Si no es así, obtienes un editor vacío, y el contenido ya se puede crear directamente en el acto. Digamos que tenemos un archivo HTML con este marcado:

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

Para crear un editor WYSIWYG vacío en una página, solo necesita unas pocas líneas en un script que se ejecuta en una página con este marcado:

//     
import Wysiwyg from 'wysiwyg';

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

Con este código, puede escribir cualquier texto desde cero. En este punto, ya puedes ver cómo funciona ProseMirror.

Cuando un usuario escribe texto en este editor, se producen transacciones de estado. Por ejemplo, si escribe la frase "Este es mi nuevo y genial editor WYSIWYG" en el editor con el esquema anterior, ProseMirror responderá a la entrada del teclado invocando el conjunto apropiado de transacciones, y una vez completada la entrada, el contenido en el estado del documento se verá así:

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

Si queremos que algún texto, por ejemplo, el contenido de un artículo ya creado anteriormente, se abra en el editor para editarlo, entonces este contenido debe corresponder al esquema creado anteriormente. Entonces el código de inicialización del editor se verá un poco 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);

¡Hurra! Creamos nuestro primer editor, que puede crear una página limpia para crear contenido nuevo y abrir los existentes para editarlos.

Qué hacer con el editor a continuación


Pero incluso con un conjunto completo de nodos, nuestro editor aún carece de funciones importantes: formatear texto, párrafos. Para esto, además de los nodos, también se debe transferir al circuito un objeto con configuraciones para formatear, marcas. Por simplicidad, los llamamos así: marcas. Y para controlar la adición, eliminación y formateo de todos estos elementos, los usuarios necesitarán un menú. Los menús se pueden agregar utilizando complementos personalizados que estilizan los objetos del menú y describen el cambio en el estado de un documento al elegir ciertas acciones.

Usando complementos, puede crear cualquier diseño que amplíe las características integradas del editor. La idea principal de los complementos es que procesan ciertas acciones del usuario para generar transacciones que les correspondan. Por ejemplo, si desea hacer clic en el icono de la lista en el menú para crear una nueva lista vacía y mover el cursor al comienzo del primer elemento, entonces definitivamente debemos describir esta transacción en el complemento correspondiente.

Puede leer más sobre la configuración de formato y los complementos en la documentación oficial , así como ejemplos muy pequeños útiles sobre el uso de las funciones de ProseMirror pueden ser muy útiles .

UPDSi está interesado en cómo integramos un nuevo editor basado en ProseMirror en un proyecto existente, hablamos de esto en otro artículo .

All Articles