> Manuales > Manual de ProseMirror para la creación de editores de texto con Javascript

Qué es ProseMirror, un framework para el desarrollo de editores WYSIWYG en Javascript para la web. Primeros pasos con ProseMirror para crear un editor de texto enriquecido sencillo.

Primeros pasos con ProseMirror

En este artículo vamos a realizar una primera aproximación para conocer y practicar con ProseMirror, un framework para crear editores de contenido enriquecido en Javascript.

Qué es ProseMirror

Es una biblioteca Javascript que no implementa un editor de texto enriquecido en particular, sino que ofrece la funcionalidad que sería necesaria para desarrollarlo por nuestra cuenta. Es decir, con ProseMirror no encontrarás un editor de texto para la web listo para usar, sino una herramienta con la que tú podrías crear un editor WYSIWYG, de contenido enriquecido, con las cosas que tú realmente necesites ofrecer.

Este framework es bastante avanzado, pero tiene una buena documentación y numerosos ejemplos que puedes usar para guiarte en tus primeros pasos. Puedes encontrarlo en la página de ProseMirror. Grandes empresas lo usan, como Atlassian, The New York Times y proyectos de referencia como CodePen, Posit y otros.

Por supuesto, en la web encontrarás multitud de editores basados en ProseMirror. Esos editores muchas veces están creados específicamente para resolver las necesidades de una web en particular y por tanto estarán personalizados para construir experiencias de usuario específicas para los sitios donde se estén utilizando. También hay editores creados por la comunidad con la intención de servir de manera genérica en aplicaciones de todo tipo. Para encontrarlos puedes hacer una simple búsqueda en Google con "editor based on prosemirror".

Una característica fundamental de Prosemirror es que no funciona produciendo HTML, el cual muchas veces se ensucia y es difícil de mantener. En cambio usa su propio modelo de datos para definir los elementos que aparecen en el área de edición. Ese modelo de datos es similar a lo que sería el DOM.

Puedes encontrar más información sobre este framework y sus posibilidades para la creación de editores de texto enriquecido en la categoría de ProseMirror.

Para trabajar con ProseMirror tenemos que usar diversos módulos con piezas de código diversas. El código de ProseMirror está muy modularizado para que sea fácil usar lo que realmente vayamos a necesitar en el editor que estemos construyendo.

Sus módulos principales son:

Primer ejemplo básico de Prosemirror

Comenzamos con la parte práctica, que nos permitirá experimentar las funcionalidades más básicas de ProseMirror y montar nuestro primer editor. Para estos primeros pasos he tomado de base las explicaciones y ejemplos que encontraremos en la guía oficial de iniciación a ProseMirror, en las que he agregado algunos puntos que ellos dejan pasar por alto y que considero que es importante mencionar para que esta primera toma de contacto resulte más sencilla.

Vamos a empezar instalando algunas dependencias.

npm i prosemirror-schema-basic prosemirror-state prosemirror-view

Luego podemos usar este código fuente que sería un interensante Hola Mundo con ProseMirror, en el que se creará con un editor muy sencillo. Vamos a ver el código completo y luego iremos comentando las partes.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Hello World Prosemirror</title>
  <style>
      #editor {
        border: 1px solid blue;
      }
      .ProseMirror {
        position: relative;
        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 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;
      }
  </style>
</head>
<body>
  <h1>Hello World Prosemirror example</h1>
  
  <div id="editor"></div>

  <script type="module">
      import { schema } from "prosemirror-schema-basic"
      import { EditorState } from "prosemirror-state"
      import { EditorView } from "prosemirror-view"

      let state = EditorState.create({ schema });
      let view = new EditorView(document.getElementById('editor'), {
        state,
      });
  </script>
</body>
</html>

Hemos colocado todo el código inline para que sea más fácil de acompañar en estos ejemplos. Como sabes, los estilos podríamos colocarlos en archivos aparte, así como el Javascript.

Tenemos una serie de estilos CSS que aplican a un elemento con clase "ProseMirror". Esa clase es la del elemento de tipo "contentEditable" que se creará dinámicamente para hacer el editor donde el usuario podrá escribir el contenido enriquecido.

Luego tenemos una etiqueta que será donde se cree el editor. Inicialmente está vacía y su contenido se creará en tiempo de ejecución cuando se genere el editor.

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

Solo cabe fijarse su identificador, porque lo vamos a usar enseguida para decir que ahí es donde se debe generar el editor wysiwyg.

Por último tenemos el script Javascript que generará el editor. En este se realizan imports, por lo que es de tipo "module".

<script type="module">
    import { schema } from "prosemirror-schema-basic"
    import { EditorState } from "prosemirror-state"
    import { EditorView } from "prosemirror-view"

    let state = EditorState.create({ schema });
    let view = new EditorView(document.getElementById('editor'), {
      state,
    });
</script>

Como contenido en el script nos traemos varias declaraciones.

En las siguientes líneas creamos el estado, con EditorState.create() enviando el schema básico. Este estado inicial será un documento en blanco y su valor corresponderá con el schema proporcionado.

Por último creamos la vista a la que le decimos dónde tiene que inyectarse (en el elemento de id="editor") y el estado del editor que acabamos de crear.

Cómo ejecutar este ejercicio en el navegador

Date cuenta que estamos importando librerías por medio del nombre del package npm, por lo tanto, necesitaremos una herramienta frontend para que resuelva esos nombres de paquetes npm.

Si ejecutas la página web simplemente haciendo un doble clic sobre el archivo no te funcionara, justamente porque el navegador no entiende esos imports con nombres de paquetes de npm. Es por ello que se necesita una herramienta para desarrollo.

Herramientas frontend habituales son Webpack, Rollup o similares. Hay una en particular llamada Vite que es super cómoda.

Puedes aprender más aquí en desarrolloweb: Categoría de Vite. También encuentras una guía útil para ejecutar este tipo de proyectos frontend en el Manual de Webpack.

Al ejecutar este archivo comprobarás que el editor que se crea no tiene prácticamente ninguna utilidad. Simplemente es un área de contenido que si pulsamos teclas del teclado se editará con el contenido tecleado.

Qué son las transacciones

Prosemirror tiene un comportamiento especial para gestionar las ediciones del contenido, implementado mediante las transacciones. Éstas consisten en objetos que describen los cambios realizados en el editor.

Con cada transacción se actualiza el estado y con luego se actualiza también la vista, una vez aplicada la transacción al estado.

Esta tarea la realiza de manera automática el propio editor, pero nosotros podemos implementar la aplicación de una transacción de manera personalizada, enviando un método llamado dispatchTransaction() cuando creamos la vista del editor. Utilizaríamos con un código como este:

let state = EditorState.create({ schema });
let view = new EditorView(document.getElementById('editor'), {
  state,
  dispatchTransaction(transaction) {
    console.log(
      "El contenido del documento tenía", 
      transaction.before.content.size,
      "y ahora tiene",
      transaction.doc.content.size);
    let newState = view.state.apply(transaction)
    view.updateState(newState)
  }
});

En el anterior código son muy importantes estas líneas:

let newState = view.state.apply(transaction)
view.updateState(newState)

En ellas puedes ver cómo se aplica la transacción y cómo se ha generado un nuevo estado gracias a ella. Luego cómo se actualiza la vista pasando este nuevo estado.

Al haber creado código personalizado para dispatchTransaction(), estamos obligados a escribir esas sentencias en el método o de lo contrario el componente no reflejará los cambios de estado.

Instalando plugins

ProseMirror organiza varias partes de su código en forma de plugins, de modo que los podemos usar o no en función de las necesidades de nuestro editor.

En el siguiente ejemplo vamos a instalar un par de plugins que aportarán algo más de funcionalidad a nuestro editor de ejemplo y nos permiten estas cosas:

Vamos a comenzar instalando los dos plugin que vamos a utilizar en nuestro editor.

npm i prosemirror-history prosemirror-keymap 

Ahora los tenemos que importar en el módulo Javascript.

import { undo, redo, history } from "prosemirror-history";
import { keymap } from "prosemirror-keymap";

Con el primer import hacemos nos traemos el plugin history, junto con los comandos undo y redo que nos permitirán pasar a los estados anteriores o siguientes del historial.

Los comandos son un tipo especial de funciones que veremos más adelante. Muchas de las acciones que podremos realizar sobre el editor se implementarán por medio de comandos.

Con el segundo import nos estamos trayendo el plugin que nos permite definir los atajos de teclado.

Seguidamente los podemos usar. Los vamos a declarar por medio de un array a la hora de crear el objeto de estado del editor.

let state = EditorState.create({ 
  schema,
  plugins: [
    history(),
    keymap({ "Mod-z": undo, "Mod-y": redo })
  ],
});

Gracias a este plugin, cuando estamos editando el texto podemos usar las teclas de Ctrl + z para undo y Ctrl + y para redo (Si estás en Mac serían las teclas Cmd + z y Cmd + y).

Estas combinaciones de teclas antes no producían ningún efecto en el editor. Puedes verificarlo quitando momentáneamente el plugin de keymap.

Conclusión

Hasta aquí hemos dado una serie de pasos para construir el editor. Nos queda mucho por delante para que sea algo usable, pero ya hemos verificado que ProseMirror nos puede ahorrar mucho trabajo de programación y ponernos las cosas más fáciles cuando necesitemos desarrollar nuestro propio componente para la edición enriquecida.

En próximos artículos vamos a avanzar un poco más sobre este sistema para el desarrollo de editores WYSIWYG con Javascript para la web.

Ahora vamos a ver el código completo de nuestro Hola Mundo en ProseMirror, después de haber agregado las útimas partes del código para trabajar con plugins y las transacciones.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Hello World Prosemirror</title>
  <style>
      #editor {
        border: 1px solid blue;
      }
      .ProseMirror {
        position: relative;
        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 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;
      }
  </style>
</head>
<body>
  <h1>Hello World Prosemirror example</h1>
  
  <div id="editor"></div>

  <script type="module">
      import { schema } from "prosemirror-schema-basic";
      import { EditorState } from "prosemirror-state";
      import { EditorView } from "prosemirror-view";
      import { undo, redo, history } from "prosemirror-history";
      import { keymap } from "prosemirror-keymap";

      let state = EditorState.create({ 
        schema,
        plugins: [
          history(),
          keymap({ "Mod-z": undo, "Mod-y": redo })
        ],
      });
      let view = new EditorView(document.getElementById('editor'), {
        state,
        dispatchTransaction(transaction) {
          console.log(
            "El contenido del documento tenía", 
            transaction.before.content.size,
            "y ahora tiene",
            transaction.doc.content.size);
          let newState = view.state.apply(transaction)
          view.updateState(newState)
        },
      });
  </script>
</body>
</html>

Para acabar, voy a enlazar con el repositorio de GitHub donde vamos a colocar todos los ejemplos de Prosemirror que estamos usando para la realización de este manual.

En el siguiente artículo vamos a abordar lo Comandos en ProseMirror que nos permitirán crear nuevos atajos de teclado para producir nuevos comportamientos en nuestro editor.

Vídeo de Prosemirror

Si prefieres aprender de manera más visual y entender paso a paso cómo comenzar a desarrollar un editor de texto enrquecido hemos publicado un vídeo que te explica cómo realizar un primer proyecto con Prosemirror.

Miguel Angel Alvarez

Fundador de DesarrolloWeb.com y la plataforma de formación online EscuelaIT. Com...

Manual