Cómo crear plugins que podremos usar en los editores de Prosemirror. Ejemplos de plugins que realizan acciones sobre el editores enriquecidos.
Para el desarrollo de nuestros propios editores es habitual crear plugins que encapsulen ciertas funcionalidades que queremos implementar dentro del editor. Estos plugins pueden ofrecer todo tipo de operaciones, desde responder a la interacción del usuario hasta realizar transformaciones adicionales mediante transacciones.
En este artículo aprenderemos a crear plugins propios y veremos alguna cosa básica de las que podríamos desarrollar a base de plugins.
En qué consiste un plugin de ProseMirror
Un plugin en ProseMirror consiste en una instancia de la clase Plugin que nos ofrece el módulo de prosemirror-state
. Por tanto, para poder crear un plugin debemos comenzar por importar la clase correctamente:
import { EditorState, Plugin } from "prosemirror-state";
Luego podremos crear la instancia de la clase para conseguir el plugin. En el constructor debemos enviar un objeto de configuración del plugin.
const miNuevoPlugin = new Plugin( {} );
Este plugin en sí no haría absolutamente nada, ya que su configuración está vacía, ya que el objeto de configuración enviado a la clase que crea el plugin en su constructor no tiene ninguna propiedad. Enseguida pondremos un ejemplo sobre cómo aplicar funcionalidades a los plugins.
Como hemos visto en artículos anteriores del Manual de ProseMirror, los plugins se añaden a un editor de texto a la hora de crear el estado, en un array de plugins.
let state = EditorState.create({
schema,
plugins: [
miNuevoPlugin,
],
});
Cómo añadir funcionalidad a un plugin ProseMirror
Ahora vamos a crear un plugin más relevante, que permite hacer alguna cosa con el editor. Para ello entregaremos en el objeto de configuración del plugin algunas propiedades. En este caso simplemente vamos a detectar los clics del usuario en el editor y mostrar en consola la posición donde ha hecho clic.
const showClickPosPlugin = new Plugin({
props: {
handleClick(view, pos) {
console.log('Se ha hecho click en la posición', pos);
return false;
}
}
})
Qué son las props de los plugins
En el objeto que enviamos al constructor del plugin usamos "props
" para añadir propiedades.
Las propiedades (props
) son valores de configuración que le podemos pasar al constructor del editor view
o podemos incluir dentro de un plugin en la propiedad "props
". Puedes ver la lista completa en la página de documentación sobre el editor view y sus props.
Los manejadores de eventos que asignamos a las props deben devolver true
en el caso que hayan manejado el evento. En ese caso la vista se encargará de llamar a preventDefault
sobre el objeto evento, salvo en el caso del manejador handleDOMEvents
, en el cuál sí es necesario realizar la invocación de preventDefault
de manera explícita dentro del manejador.
Esto quiere decir que los manejadores que puedan encargarse de una misma tarea se pueden ir invocando uno a uno hasta que uno de ellos devuelve true
, en cuyo caso se paran de invocar nuevos manejadores. La precedencia comienza con las props
definidas en el editor view
y luego en adelante en el orden en el que se registraron los plugins.
Los manejadores de eventos de la vista
En el plugin anterior hemos indicado un método en props que se llama `handleClick()`` que es el manejador de eventos que se ejecutará al hacer clic sobre el editor.
Cada manejador recibe una serie de parámetros que podemos ver especificados en la documentación. En el caso de `handleClick()`` encontramos que se envían muchos otros datos que no hemos llegado a usar en nuestro ejemplo. Solamente estamos recibiendo el primer y segundo parámetro, siendo el segundo la posición donde se ha hecho clic del editor.
La posición es un valor entero que indica el número de caracteres desde el inicio del contenido editable.
En el plugin simplemente volcamos el contenido de la posición a la consola. Además enviamos false
como respuesta del manejador, para indicar que no hemos manejado nada en este evento y que otros plugins puedan seguir manejando este clic.
El estado en los plugins
Al crear un plugin podemos inicializar un estado y ese estado puede persistir durante la vida del editor. Para esto usaremos la propiedad state
del objeto de creación del plugin.
let countTransactions = new Plugin({
state: {
init() {
return 0;
},
apply(tr, value) {
console.log('Detectada transacción número', value + 1);
return value + 1;
}
}
});
En el código anterior tenemos el state definido con dos métodos:
- El método `init()`` sirve para inicializar el valor del estado
- El método `apply()`` se invoca después de cada transacción y permite devolver un nuevo valor para este estado
En este caso, estaremos realizando una simple cuenta de las transacciones que se han realizado sobre un editor.
En el método `apply()`` podríamos analizar la transacción y devolver el nuevo valor del estado que se debe persistir. Como otras partes de ProseMirror, los estados son inmutables, por lo que ese valor generará un nuevo estado.
Añadir controles en la vista del editor
Ahora vamos a hacer un ejercicio de creación de un plugin en el que vamos a añadir un botón a la vista del editor.
Cuando necesitamos acceder a la vista del editor, por ejemplo para añadir una interfaz al editor, como podría ser un botón, vamos a usar el método `view()`` del objeto de configuración del plugin.
El método view()
se invoca una vez, en el proceso de inicialización del editor. Podremos realizar en él todas las acciones necesarias para generar la interfaz que vamos a colocar en el editor y tenemos que devolver un objeto de tipo PluginView cuya interfaz debe contener al menos un método update()
, que se invocará cada vez que se realice cada transacción durante el uso del editor.
Para mostrar un ejemplo sencillo de esta operativa realizaremos un nuevo ejemplo de plugin en el que vamos a crear un botón, asignarle una funcionalidad de "negrita". Luego lo añadiremos al editor. La funcionalidad del botón la conseguiremos mediante la asociación de un manejador de evento "click" al botón, para producir un cambio de la "mark" "strong" (negrita) en la selección actual, con lo que estaremos haciendo un botón que nos cambie el estado de negrita del editor, alterando el texto que tengamos seleccionado o permitiendo escribir en negrita a partir de entonces.
El código de este plugin ahora se complica un poco más. Lo vamos a ver todo de una y trataremos de explicar algunas de sus claves.
let boldButtonPlugin = new Plugin({
view(editorView) {
const element = document.createElement('button');
element.textContent = 'Bold';
const boldCommand = toggleMark(schema.marks.strong);
element.addEventListener('click', function() {
boldCommand(editorView.state, editorView.dispatch);
editorView.focus();
})
editorView.dom.parentNode.insertBefore(element, editorView.dom);
return {
update() {
console.log('plugin view update');
}
};
}
})
Al definir el objeto de configuración del plugin usamos el método `view()``, que recibe el la vista del editor.
Dentro del método view realizamos estos pasos:
- Creamos un elemento de tipo
button
, mediante el método `document.createElement()`` del navegador. - Luego ponemos algo de texto en el botón. Como ves, la creación de interfaces y su acceso se realiza mediante Javascript vanilla.
- Luego creamos un comando de tipo `toggleMark()``. Esto lo explicamos en el artículo de hacer un botón de bold, solo que en esa ocasión el botón se encontraba fuera del editor y ahora lo hemos generado dentro, por medio de un plugin que podríamos reutilizar.
- Luego añadimos un manejador de evento "click" para el elemento botón
- Dentro del manejador ejecutamos el comando
boldCommand
, el cual necesita recibir el estado de la vista y una función de despacho de las transacciones, que obtenemos también de la vista del editor. - Luego usamos el método
insertBefore
del DOM del editor, indicando qué elemento queremos situar antes de la vista del editor. - Por último devolvemos un objeto que tiene un método `update()``, que se invocará con cada transacción y que tiene como objetivo actualizar la vista del plugin si fuera necesario. En nuestro caso no queremos actualizar nada, por lo que ese método solo muestra un mensaje en la consola.
Eso es todo! Gracias a este plugin hemos podido incorporar un botón para poner en negrita, que funcionará en cualquier editor donde asociemos este plugin.
Lo que hemos visto en este último plugin es la base mediante la cual podremos crear las interfaces de barras de herramientas en los editores, solo que en los casos reales tendremos varios botones con diversas acciones y no solamente uno como hemos visto aquí.
Construir un menú completo para un editor de ProseMirror
El paso de construir un menú completo no es trivial, pero con lo que hemos visto hasta aquí tenemos una buena idea de las piezas que necesitamos para construirlo:
- Un plugin que trabaje con el método
view()
- La creación de todo el marcado para conseguir los botones del menú y su incorporación a la interfaz del editor
- Una serie de comandos necesarios para las acciones de cada botón
- La asignación de los manejadores de eventos sobre los elementos del menú, que invocarán los correspondientes comandos
Existe un ejemplo en la documentación de ProseMirror que muestra cómo conseguir todas esas acciones con un código bastante ordenado y modular. Puedes consultarlo en la página "Adding a menu".
A veces los ejemplos dentro del sitio de ProseMirror son difíciles de reproducir, porque ellos no te muestran el contexto donde utilizar todos esos snippets del artículo del ejemplo, así que si lo deseas puedes ver este ejemplo montado en nuestro repositorio de ejemplos de ProseMirror. Encontrarás el ejemplo en el archivo src/ejemplos/06-menu.html
.
No vamos a dar más explicaciones de este código porque preferimos avanzar en otros frentes y explicar algo tan importante como Obtener el código HTML que hay en un editor ProseMirror. De todos modos al menos tienes ahí los ejemplos por si deseas investigarlo por tu cuenta.
Miguel Angel Alvarez
Fundador de DesarrolloWeb.com y la plataforma de formación online EscuelaIT. Com...