Editor de texto enriquecido con JavaScript y TinyMCE

Aprende a crear un editor de texto enriquecido con JavaScript y TinyMCE, para incluir un módulo WYSIWYG en tu aplicación web. 

En el último bloque de éste escrito he preparado un ejercicio práctico. Allí te enseño a programar la interfaz de un foro ficticio que hace uso de TinyMCE.

Haz click en la siguiente imagen para ver el resultado final:

Ver ejercicio terminado. Implementa un módulo WYSIWYG con TinyMCE. Haz click sobre la imágen
Ver ejercicio terminado. Implementa un módulo WYSIWYG con TinyMCE

Si quieres ir directamente a realizar la práctica, haz click aquí

A pesar de que no es el tema principal de este blog, en muchas ocasiones escribo sobre interfaces gráficas y experiencia de usuario.

Es algo inevitable, ya que como imaginarás, el desarrollo frontend va muy ligado a estas disciplinas. 

De hecho, muchas de las librerías que ya he analizado por aquí, existen para resolver problemas de UI/UX.

El caso de hoy es otro gran ejemplo de ello. En esta ocasión te voy a hablar de TinyMCE, una librería JavaScript para el desarrollo de editores de texto enriquecidos para web.

¿Qué es un texto enriquecido?

A diferencia de una cadena de texto plano, el texto enriquecido está formateado de tal modo, que almacena estilos y estructura en sí mismo.

En realidad no es un concepto nuevo, con un par de ejemplos enseguida entenderás a qué me refiero.

A diferencia de un documento con extensión «.txt», que solo contiene texto plano, un archivo «.docx» es capaz de guardar información adicional de estilos, como negritas, subrayados, tamaños de texto, y mucho más.

Otro ejemplo es el propio lenguaje HTML. El marcaje basado en etiquetas, así como los estilos en línea de estas, hacen que el contenido HTML pueda visualizarse de forma «enriquecida».

Y es precisamente este segundo tipo, donde TinyMCE hace hincapié. En seguida veremos en qué sentido.

WYSIWYG o cómo generar código HTML sin escribir una sola línea de código. 

Ofrecer la posibilidad de generar texto enriquecido HTML, puede ser de enorme utilidad en aplicaciones web, como por ejemplo blogs o foros

Imagínate cómo de aburrido y plano sería leer artículos sin ningún tipo de estilo o estructura.

Por eso, en un blog, es imprescindible que los redactores publiquen sus escritos en formato HTML.

Solo hay un pequeño inconveniente, no todo el mundo conoce el lenguaje.

Es más, aún conociéndolo, redactar texto teniendo que incluir el etiquetado propio del código, se vuelve una tarea tediosa rápidamente. 

Como dije, aquí es donde entra en juego TinyMCE. Se trata de un editor WYSIWYG de las siglas en inglés «What You See Is What You Get». 

Crea un módulo WYSIWYG con JavaScript y TinyMCE
Crea un módulo WYSIWYG con JavaScript y TinyMCE

Tal y como su nombre indica, el objetivo de estos módulos es ofrecer al usuario una interfaz, que le permita de forma muy visual, formatear texto en código HTML.

Al final del día, eso se traduce en poder incluir recursos multimedia, agregar estilos a textos, incluir enlaces y mucho más, todo sin escribir una sola línea de código. 

Las capacidades de TinyMCE

La librería JavaScript TinyMCE existe desde nada más y nada menos que desde 2004.

Por supuesto no es el único WYSIWYG que puedes encontrar en npmjs.org, pero sí uno de los más populares, sin duda.

Al final del artículo he listado una serie de alternativas nada desdeñables.

TinyMCE fué desarrollado y actualmente mantenido por el equipo de la empresa Tiny. Su repositorio en Github tiene más de 12.2K estrellas.

La media de descargas semanales asciende a 405.476

Las capacidades de ésta herramienta son muchas, pero además, está planteada de tal modo, que admite extensiones para ampliar sus funcionalidades.

De hecho, existe una larga lista de plugins a disposición de cualquier programador.

Aunque es importante destacar que algunos están vinculados a una licencia «premium» de pago.

De entre las funciones que se pueden incluir de base, podemos encontrar las siguientes:

  • Añadir imágenes
  • Crear enlaces
  • Autoguardado
  • Pantalla completa
  • Generar tablas
  • Tabulación
  • Bloques visuales

Tienes un listado completo, segmentado por usos de pago o gratuito, en el siguiente enlace:

Plugins y funciones de la librería 

Integrar este recurso en tu proyecto se puede hacer de muchas formas. 

No obstante, en ésta guía me centraré únicamente en la instalación auto-alojada mediante NPM.

Instalar la librería TinyMCE e implementar su API. 

Con el siguiente comando se puede instalar la librería. 

npm i tinymce

Una vez se encuentre entre las dependencias del proyecto, debemos importar este listado de módulos. 

import tinymce from "tinymce";
import "tinymce/icons/default";
import "tinymce/themes/silver";
import "tinymce/models/dom";

Es necesario importar estos recursos adicionales ya que se tratan de dependencias internas de la librería.

Para evitar sobrecargar de peso, TinyMCE solo viene de serie con las funciones básicas de un editor de texto enriquecido.

Eso significa que si queremos agregar funciones extras, deberemos importar “plugins” adicionales.

Por ejemplo, para que el editor permita incluir tablas, habrá que incluir la extensión “table” al inicio del script.

import "tinymce/plugins/table";

Para generar un editor de texto nuevo, solo hace falta llamar al método tinymce.init(opciones)”, pasándole un objeto de configuración como argumento.

tinymce.init({
  selector: '#my-textarea',
  plugins: [
    "table",
  ]
});

Mediante la propiedad “selector”, indicamos qué elemento del DOM va a actuar de contenedor para el editor. 

«Selector» admite una cadena de texto en forma de selector CSS.

El resto de propiedades son opcionales pero vale la pena destacar algunas de las mas relevantes.

  • selector: Como hemos visto permite conectar la instancia al elemento del DOM, mediante un selector CSS.
  • target: Si deseas pasar tu mismo la referencia al DOM (por ejemplo mediante document.querySelector()) puedes hacerlo con la propiedad “target”. En tal caso debes eliminar “selector”.
  • inline: Admite una booleana. Si se define como true, el editor WYSIWYG se posicionará para cada elemento del contenedor indicado.
  • auto_focus: Al pasarle un texto con el id de un elemento del DOM, se activará el estado de “focus” automáticamente.
  • promotion y branding: Por defecto TinyMCE muestra botones e información relativa a la plataforma Tiny. Se pueden desactivar ambas configurando sus valores como false.
  • plugins: Se trata de una array con el nombre de todas aquellas extensiones que se desean activar (y que se hayan importado previamente)
  • toolbar: Una cadena de texto que permite ubicar botones con accesos rápidos a acciones de forma declarativa.
  • language: Código del idioma con el que se quiere traducir la interfaz (por defecto en_US)

Evidentemente hay muchas más opciones, así que a continuación te dejo un enlace a la documentación completa.

Documentación completa de TinyMCE

Hay que tener en cuenta que al ejecutar “tinymce.init()” devuelve una promesa.

Por ese motivo, puede ser interesante enmarcar su llamada dentro de una función asíncrona. Posteriormente se puede utilizar el método “tinymce.get(id o número)” para obtener el editor que se ha generado

const launchTinyMCE = async (selector) => {
  try {
    await tinymce.init({
      selector,
      plugins: [
        "table",
      ]
    });
    return tinymce.get(0); //devuelve un editor
  } catch (error) {
    console.log(error);
  }
};

Existe una enorme variedad de métodos y propiedades, tanto en el objeto “tinymce” como en los editores que éste genera.

De nuevo, solo voy a destacar algunas de las más relevantes, puedes encontrar el resto en la documentación oficial que te facilité.

  • tinymce.get(id o número) devuelve la instancia de editor generada que corresponda con el id indicado o índice en el array interno de la librería.
  • tinymce.remove(editor | string | objeto) destruye uno o varios editores.
  • createEditor() Otra forma de generar un nuevo editor.

A nivel de editor, algunos métodos relevantes son:

  • editor.resetContent() Reinicia el contenido del editor.
  • editor.focus() Fuerza el estado de “focus” sobre un editor específico.
  • editor.on() Asigna eventos y funciones de respuesta sobre un editor.
  • editor.getContent() Devuelve el contenido generado en el editor.

Incluir un editor de texto enriquecido en el hilo de un foro ficticio

Ha llegado el momento de poner en práctica lo aprendido. 

Y no se me ocurre mejor forma que creando la interfaz de un foro ficticio, donde los usuarios hagan uso de TinyMCE para añadir nuevos posts a un hilo abierto.

Aunque TinyMCE da soporte a múltiples frameworks, como React o Angular, en este ejercicio lo trabajaremos con «Vanilla JavaScript». 

Te dejo el enlace al código finalizado, por si lo quieres tener de referencia a lo largo de este tutorial.

Código fuente del ejercicio completo

Comenzamos preparando una estructura HTML base. En ella habilitamos tres áreas principales, una cabecera, la sección de posts y un pié de página.

La cabecera y el “footer” solo sirven para dar un toque corporativo al ejercicio.

<div class="top-bar">
  <span><img src="./images/chat.png" alt="" /></span>
</div>
<footer>
  Logo by
  <a
    href="https://www.flaticon.es/iconos-gratis/hablando"
    title="hablando iconos"
    target="_blank"
    rel="nofollow"
    >Hablando iconos creados por Freepik - Flaticon</a
  >
</footer>

El contenedor “forum-view” contiene, a su vez, tres secciones más.

El primer bloque solo muestra una breve descripción:

<div class="description">
  <h1>Bienvendio al foro</h1>
  <p>
    Sé respetuoso en todo momento con el resto de usuarios de la
    plataforma
  </p>
</div>

El área que sigue, es donde se iran listando nuevos posts, de hecho ponemos uno “hardcoded” a modo de ejemplo.

<div class="thread-container">
  <div class="post-container">
    <div class="post-header">
      <span class="snap-container">
        <img
          src="https://randomuser.me/api/portraits/men/52.jpg"
          alt=""
        />
      </span>
      <span class="username"> <strong>@userexample</strong> dijo:</span>
    </div>
    <div class="post-body">
      <p>
        Para aprender React / Vue / Angular o cualquier otro framework web
        es necesario aprender primero JavaScript.
      </p>
      <p>
        <strong>¿Qué pensáis?</strong>
      </p>
    </div>
  </div>
</div>

Y finalmente un formulario, con un único campo de texto que actuará de contenedor para TinyMCE

<div class="form-container">
  <form id="post-form" class="new-novel-part-form">
    <textarea
      name=""
      id="main-input"
      cols="30"
      rows="5"
      placeholder="Escribe tu respuesta aquí, puedes incluir imágenes, tablas, listas y hasta emojis 😎"
      autofocus
    ></textarea>
    <button type="submit">Publicar</button>
  </form>
</div>

No me detendré demasiado a explicar los estilos SASS, sin embargo, puedes dedicar todo el tiempo que quieras a estudiarlos detenidamente.

@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@300;700&display=swap')
:root
  --main-color: #2e8de6
  --darker-main-color: #2b7ac3
*
  box-sizing: border-box
strong
  font-weight: 700
body
  font-family: 'Montserrat', sans-serif
  margin: 0
  padding: 0
  color: rgb(21, 21, 21)
  box-sizing: border-box
  background-color: #ededed
  .top-bar
    display: flex
    justify-content: center
    align-items: center
    background-color: var(--main-color)
    padding: 12px 5px 12px 5px
    box-shadow: 0 3px 5px rgba(0, 0, 0, 0.2)
    border-bottom: solid 3px var(--darker-main-color)
    & > :nth-child(1)
      width: 200px
      height: 95px
      overflow: hidden
      img
        width: 100%
        height: 100%
        object-fit: contain
        object-position: center center
        display: block
  .forum-view
    max-width: 1000px
    margin: 0 auto 0 auto
    padding: 30px 30px
    .description
      background-color: #b6dcff
      padding: 25px 0
      margin: 0 0 20px 0
      color: #001e39
      text-align: center
      h1
        margin: 0 0 10px 0
      p
        margin: 0px
    .thread-container
      .post-container
        position: relative
        background-color: #fff
        box-shadow: 0 3px 5px rgba(0, 0, 0, 0.2)
        font-size: 1.3rem
        margin: 0 0 20px 0
        border-radius: 10px
        overflow: hidden
        .post-header
          background-color: var(--darker-main-color)
          padding: 10px
          display: flex
          align-items: center
          color: #fff
          font-size: .9rem
          .snap-container
            display: block
            overflow: hidden
            border-radius: 100px
            width: 70px
            border: solid 2px #08457e
            img
              display: block
              width: 100%
          .username
            display: block
            margin: 0 0 0 20px
        .post-body
          padding: 20px
    .form-container
      button[type="submit"]
        display: block
        width: 100%
        background-color: var(--main-color)
        border: none
        border-radius: 5px
        box-shadow: 0 3px 5px rgba(0, 0, 0, 0.3)
        font-size: 1.5rem
        color: #FFF
        cursor: pointer
        padding: 15px 0
footer
  background-color: var(--darker-main-color)
  color: #fff
  font-size: .8rem
  text-align: center
  padding: 20px
  a
    color: #fff
@media screen and (max-width: 600px)
  body
    .login-view
      display: block
      padding: 20px
      img
        width: 100%

Con el objetivo de mantener una coherencia gráfica, también vamos a generar otras hojas CSS, que sobreescriban algunos estilos predeterminados del editor.

En este caso, pero, vamos a utilizar una herramienta que el equipo de Tiny ofrece gratuitamente

Se trata de una interfaz para seleccionar colores, tipografía y demás acabados gráficos.

Editor de skins para TinyMCE

Editor online de "skins" para tinymce
Editor online de «skins» para tinymce

Una vez tengas el «skin» de tu editor configurado, haz click en «get skin», e incluye el directorio «skins» en tu carpeta de recursos públicos.

Más tarde, estableceremos una referencia a esos estilos.

Por último, pero no por ello menos importante, programamos el comportamiento con JavaScript.

Crea un directorio con el nombre «services», y en su interior un archivo llamado «launchTinyMCE.js«

Utilizaremos este script para preparar la instancia del editor, de forma aislada.

Al inicio importamos los módulos necesarios.

import tinymce from "tinymce";
/* Default icons are required. After that, import custom icons if applicable */
import "tinymce/icons/default";
/* Required TinyMCE components */
import "tinymce/themes/silver";
import "tinymce/models/dom";

Seguidamente, importamos los plugins que deseamos habilitar en nuestro editor

En mi caso he incluido los siguientes, pero siéntete libre de añadir cuantos quieras.

import "tinymce/plugins/fullscreen";
import "tinymce/plugins/media";
import "tinymce/plugins/help";
import "tinymce/plugins/code";
import "tinymce/plugins/emoticons";
import "tinymce/plugins/emoticons/js/emojis";
import "tinymce/plugins/link";
import "tinymce/plugins/lists";
import "tinymce/plugins/table";
import "tinymce/plugins/image";
import "tinymce/plugins/preview";

Declaramos una función asíncrona, que reciba un selector, y llame al método «tinymce.init()» con una configuración básica.

const launchTinyMCE = async (selector) => {
  try {
    await tinymce.init({
      selector,
      auto_focus: "main-input",
      promotion: false,
      branding: false,
      skin: "CUSTOM",
      content_css: "CUSTOM",
      plugins: [
        "emoticons",
        "lists",
        "link",
        "image",
        "preview",
        "code",
        "fullscreen",
        "media",
        "table",
        "help",
      ],
      toolbar: `
        undo redo | 
        blocks |
        image |
        bold italic backcolor | 
        alignleft aligncenter alignright alignjustify |
        bullist numlist outdent indent | 
        removeformat |
        fullscreen help
      `,
    });
    return tinymce.get(0);
  } catch (error) {
    console.log(error);
  }
};
export default launchTinyMCE;

De este código es importante destacar algunos puntos.

  • Se ha forzado a que el editor esté en estado de «focus» con la propiedad auto_focus.
  • Se han eliminado los botones y marca de la empresa Tiny, con «promotion» y «branding» en false.
  • Se ha referenciado la interfaz personalizada con «skin» y «content_css» (asegurate de que coincide con el nombre de tu directorio)
  • Se han incluido los plugins importados 
  • Se ha construido una barra de herramientas con los accesos rápidos con «toolbar».

Llegados a este punto, sería suficiente para tener un editor de texto enriquecido totalmente funcional. No obstante, hay un par de ajustes más, que considero importantes.

Por un lado, el editor se construirá totalmente en inglés, por eso, añadiremos una traducción a otra lengua.

Accede al siguiente enlace, y descarga el paquete de traducciones que deses. 

Listado de descarga de traducciones

Incluye tu directorio «langs», en la raíz pública de tu proyecto. 

A continuación, añade la propiedad «language» con el código de idioma correspondiente, en el objeto de inicialización.

...
content_css: "CUSTOM",
language: "es",
plugins: [
  "emoticons",
  "lists",
  "link",
  "image",
  "preview",
  "code",
  "fullscreen",
  "media",
  "table",
  "help",
],
...

Por otro lado, para que el plugin de subida de imágenes funcione, es necesario habilitar un servidor dónde publicarlas.

No obstante, incluyendo el siguiente código en la propiedad «images_upload_handler», podemos cargar y renderizar cualquier imágen en base64.

...
content_css: "CUSTOM",
language: "es",
images_upload_handler: (blobInfo, progress) =>
new Promise((resolve, reject) => {
  resolve(`data:image/png;base64, ${blobInfo.base64()}`);
}),
plugins: [
  "emoticons",
...

Ahora sí, damos por terminado nuestro servicio para generar editores tinymce.

Seguimos ampliando nuestro programa para hacer uso de él.

Crea otro servicio dentro de la carpeta «services» que se llame «postTemplate.js»

Éste sencillamente se encargará de crear un elemento DIV nuevo con un contenido dado. Como curiosidad he incluído la característica de que muestre la imagen de un usuario aleatorio mediante la plataforma «randomuser.me»

const postTemplate = (body) => {
  const template = `
    <div class="post-header">
      <span class="snap-container">
        <img
          src="https://randomuser.me/api/portraits/${
            Math.random() > 0.5 ? "men" : "women"
          }/${Math.floor(Math.random() * (100 - 1 + 1) + 1)}.jpg"
          alt=""
        />
      </span>
      <span class="username"><strong>@userexample</strong> dijo:</span>
    </div>
    <div class="post-body">
    ${body}
    </div>
  `;
  const divPost = document.createElement("div");
  divPost.classList.add("post-container");
  divPost.innerHTML = template;
  return divPost;
};
export default postTemplate;

Para terminar conectamos los dos recursos al script principal.

En las primeras líneas se importa todo lo necesario. 

import "./style.sass";
import postTemplate from "./services/postTemplate";
import launchTinyMCE from "./services/launchTinyMCE";

Seleccionamos nuestro formulario y el área de posts del DOM con «querySelector» y los guardamos en variables.

const form = document.querySelector("#post-form");
const threadContainer = document.querySelector(".thread-container");

Declaramos, y ejecutamos una función asíncrona llamada init(). 

const init = async () => {
  const richTextEditor = await launchTinyMCE("#main-input");
  console.log(richTextEditor);
  form.addEventListener("submit", (e) => {
    e.preventDefault();
    const postBody = richTextEditor.getContent();
    if (!postBody) {
      return;
    }
    const post = postTemplate(postBody);
    threadContainer.appendChild(post);
    richTextEditor.resetContent();
  });
};
init();

En el interior de ésta función ejecutamos «launchTinyMCE()» pasando el selector CSS que corresponda.

Una vez obtenemos la instancia del editor, incluimos la escucha del evento «submit» en el formulario.

Cada vez que el usuario ejecute ese evento, generamos un post nuevo con los contenidos del editor gracias a «richTextEditor.getContent()» y «postTemplate()».

Conclusiones y recursos adicionales

Con esto damos por cerrado este análisis de la librería Tinymce. Espero que te haya sido de ayuda.

Si este tipo de librerías te han parecido interesantes, no puedes perderte AGGrid, una herramienta capaz de convertir una aburrida etiqueta TABLE, en un componente realmente potente

Crear grids dinámicos con AGGrid

Por otra parte, hace cuestión de unos meses preparé un tutorial para crear una novela colaborativa con JavaScript y Firebase.

Crear una novela colaborativa

Creo que podría ser interesante implementar un editor de texto enriquecido en ella, aquí dejo el reto.

También te dejo algunos recursos adicionales que te pueden ser de ayuda, si necesitas más información sobre Tinymce.

Hasta la próxima!

Deja un comentario