Pasar al contenido principal

Widgets de CKeditor: ¿una alternativa a Paragraphs?

Es bastante común que los editores quieran poder introducir en sus contenidos fragmentos de HTML predefinidos (componentes e incluso layouts).

En algunos sitios web, como la prensa online, estas piezas de código pueden ir sumándose hasta convertirse en auténticas librerías de componentes sencillos y reutilizables: citas, cuadros informativos, call-to-action, etcétera.

El primer impulso de todo drupalista es acudir a combinaciones de fields + field groups y/o paragraphspasando por alto una solución que en muchas ocasiones es más sencilla y fácil de mantener, igual de sólida y que proporciona grandes ventajas de cara al usuario final: los widgets de CKeditor.

¿Qué es un widget?

Ante la necesidad de ofrecer un conjunto de formatos predefinidos es habitual usar los estilos de CKeditor, los cuales nos permiten asociar un elemento HTML y una clase (o ID, o incluso CSS inline). Al hacer click en este estilo, CKeditor intenta convertir el elemento en foco. 

Configuración de estilos de Ckeditor

Un widget es una solución más completa y avanzada para este problema:

  1.  Permite cualquier estructura HTML.

    Mientras que el estilo se aplica a un único elemento, un widget permite cualquier marcado. Además, CKeditor trata al widget como un conjunto y mantiene su estructura intacta. Esto evita un problema común por el que un estilo aplicado se puede alterar involuntariamente al editar el texto, resultando en HTML malformado o roto que CKEditor filtra y elimina. 
  2. Es configurable, arrastrable y usable. 

    Mientras que un estilo puede ser muchas veces invisible, el widget se distingue claramente por su outline amarillo y el icono de arrastrar.
  3. Se puede presentar prerellenados.

    Los estilos se aplican sobre el texto y no se pueden prerellenar para indicar al usuario qué se espera. Los widgets se pueden inicializar ya completos, indicando al usuario cómo es el componente y qué debe introducir.
  4. Se pueden limitar contextualmente las etiquetas HTML permitidas.

    Esto es extremadamente útil a la hora de otorgar poder al usuario sin que esto resulte en HTML malformado, semánticamente erróneo o que altere nuestro diseño. Se puede indicar además una configuración diferente para cada zona editable.

Un caso de widget sencillo: call-to-action.

Imaginemos que nuestro cliente solicita poder insertar libremente un call-to-action (CTA)  de este estilo:

Un call-to-action

En un principio, este CTA se soluciona mediante un paragraph dividido en tres campos: un texto sin formato para el título, un texto con formato para el cuerpo y en campo de tipo enlace para el botón:

Propueta de paragraph

Sin embargo, este ejemplo es un buen candidato a ser implementado en su lugar como un widget, siempre y cuando podamos confirmar que se trata de un componente puramente visual que no necesita interactuar con Drupal (por ejemplo, el enlace no necesita autocompletado o referencia a entidades u otro tipo de integración con Drupal).

A continuación, mostraremos sin entrar en muchos detalles cómo se desarrollaría este mismo componente como un widget.

1 - Crear el plugin de Drupal

El primer paso es crear un módulo y, dentro de este, un plugin de CKeditor. Para el propósito de este tutorial vamos a usar el código autogenerado mediante Drupal Console o Drush (a partir de la versión 9):

drupal generate:plugin:ckeditorbutton
drush generate plugin-ckeditor



Si usas Drupal Console, cuidado: en ciertas versiones algunos valores necesitan ser corregidos. También puedes seguir este artículo si prefieres hacerlo a mano.

En su implementación más minimalista, el plugin de Drupal simplemente tiene que informar de la dirección del icono y el archivo plugin.js, y el resto ya es responsabilidad de CKeditor. Nos bastará con esto.

Es el momento de acudir a la configuración de nuestros editores de texto y arrastrar el nuevo botón a nuestra barra de herramientas.

El botón, añadido a CKeditor

2 - Crear el widget de CKeditor

Llegados a este punto tenemos un botón que no hace nada. Es hora de convertirlo en un widget de CKeditor que permita insertar nuestro componente.

CKEditor tiene un excelente tutorial donde cuenta paso a paso como crear un widget por lo que aquí nos vamos a centrar solo en algunos detalles.

Lo primero que necesitamos saber es que un widget es un tipo especial de plugin de CKeditor que a su vez depende del plugin 'widget' y que no implementa los métodos habituales, por lo que tendremos que añadir la dependencia y (si hemos usado el generador) eliminar la llamada a editor.addCommand().

Pero, sobre todo, para integrar correctamente con Drupal tenemos que realizar una llamada explícita a editor.ui.addButton(), algo que no aparece en el tutorial oficial de CKeditor y que puede ser el origen de más de un quebradero de cabeza.

CKEDITOR.plugins.add( 'cta_widget', {
    // Necesitarás añadir esta dependencia:
    requires: 'widget',

    // Comprueba que tiene el mismo id que tu botón:
    icons: 'cta_widget',

    init: function( editor ) {
        
        // Añade esta llamada dentro de init
        editor.ui.addButton('cta_widget', {
            label: 'CTA Widget',
            id: 'cta_widget',
            command: 'cta_widget',
            icon: this.path + 'icons/cta.png'
        });
    }
});



Una vez realizado esto, solo nos quedará configurar el widget usando el método editor.widgets.add(), que tiene esta forma:

editor.widgets.add( 'cta_widget', {
    template: ''
    editables: {},
    allowedContent: ''
    upcast: function( element ) {},
});

3 - Definir nuestro HTML

La propiedad template es una cadena de caracteres que se encargará de definir el marcado HTML deseado. En este string debemos de incluir el marcado completo, incluyendo tanto los elementos editables como aquellos que no lo serán. En nuestro caso, podría ser algo así:

template:
    '<div class="cta">' + 
        '<h3 class="cta__title">Insert title here...</h3>' + 
        '<p class="cta__text">Insert your text here</p>' + 
        '<div class="cta__link"><a href="#">Double click to edit link</a></div>' + 
    '</div>',



En este ejemplo se proporciona también un texto por defecto. Esto nos permite prerellenar el widget con valores que indiquen al usuario qué tipo de información se espera recibir.

Si en este momento acudimos a nuestro CKeditor y pulsamos el botón, se nos debería de insertar este HTML y, si hemos cargado correctamente el CSS, deberíamos verlo bien estilado. Sin embargo, en este estado todavía no es editable: podría ser funcional si lo que queremos es insertar fragmentos de HTML inmutables, pero en la mayoría de ocasiones no nos bastará.

4 - Crear zonas editables y restringir el contenido.

La siguiente propiedad de editor.widgets.add() nos permite definir un objeto en el que nombrar nuestras diferentes zonas editables y configurarlas a nuestro gusto. Para nuestro caso, una buena configuración es la siguiente:

editables: {
    title: {
        selector: '.cta__title',
        allowedContent: 'strong em',
    },
    content: {
        selector: '.cta__text',
        allowedContent: 'strong em mark br; a[!href]',
    },
    link: {
        selector: '.cta__link',
        allowedContent: 'a[!href]',
    }
},

Como se puede ver, estamos mapeando elementos HTML como <h3 class="cta__title"> a entradas como title, identificadas por un selector. Además, hacemos uso de una interesantísima funcionalidad que es la capacidad de definir el tipo de contenido que cada zona editable permite.

La propiedad allowedFormats utiliza la sintaxis especial de Allowed Content Rules de CKeditor y nos permite restringir el HTML permitido, una funcionalidad muy útil y mucho más granular que los formatos de texto de Drupal.

En este caso, estamos permitiendo en el título el uso de <strong> y <em>, lo cual suele ser un requerimiento que da bastantes problemas en Drupal.

En el cuerpo de texto permitimos el uso de unos pocos elementos más y en el del botón solo permitimos enlaces.

En este momento, el widget ya debería ser plenamente funcional y debería permitir al usuario crear, editar e incluso arrastrar y mover cada instancia generada:

En este punto, alguien podría sugerir que el elemento editable .cta__link debería ser directamente un <a> en lugar de un <div>. Es cierto, pero no es posible con la configuración estándar ya que solo los elementos definidos en CKEDITOR.$dtd.editable pueden convertirse en zonas editables.

Es posible cambiar esta configuración para añadir los elementos de tipo <a>, pero esto excede el propósito de este tutorial, por lo que lo dejaremos así de momento.

5 - El toque final: autodetección y permisos

Tal y como está, el widget tiene aún dos posibles problemas.

Por una parte, si estamos usando el filtro "Limita las etiquetas HTML permitidas y corrige el HTML incorrecto", es muy posible que al guardar el contenido o pasar al modo "Fuente HTML" se nos pierdan las clases CSS y con ellas el diseño. Esto es debido al sanitizado de CKEditor del que hemos hablado antes.

Si estuviéramos en una configuración estándar de CKEditor, bastaría con añadir la siguiente configuración a editor.widgets.add() y ésta se encargaría de añadirlo a la lista blanca de CKeditor:

allowedContent: 'h3(cta__title); div(cta__text, cta__link)',

Pero la realidad es que esto no basta, ya que Drupal implementa a su manera la configuración de CKEditor y como parece que de momento no hay un buen modo de añadir nuevas reglas programáticamente, tendremos que añadirlo manualmente en la configuración de cada editor de esta manera:

Configuración de CKEDITOR

Además, ahora mismo la simple inserción del HTML de nuestro call-to-action no significa que se instancie automáticamente. Es decir, en el caso de que copie y pegue directamente el HTML de nuestro componente el usuario podría querer editarlo y encontrarse con que no es posible.

Esto se soluciona rápidamente implementando el método upcast(), el cual no es obligatorio, pero sí muy recomendable:

upcast: function( element ) {
    return element.name == 'div' && element.hasClass( 'cta' );
},



Cada vez que procesa el HTML, CKEditor itera sobre el HTML elemento por elemento y nos da una oportunidad de indicar que queremos inicializar nuestro widget. En este caso, estamos indicando a CKEditor que intente construir el widget si encuentra un <div> con la clase .cta. El editor comprobará si este elemento coincide con nuestro template y, de ser así, lo inicializará.

Nuestro código final quedaría así:

CKEDITOR.plugins.add( 'cta_widget', {
    // Dependencia del plugin "widget"
    requires: 'widget',
    icons: 'cta_widget',
    init: function( editor ) {
        editor.widgets.add( 'cta_widget', {
            template:
                '<div class="cta">' + 
	                '<h3 class="cta__title">Insert title here...</h3>' + 
                    '<p class="cta__text">Insert your text here</p>' + 
                    '<div class="cta__link"><a href="#">Double click to edit link</a></div>' + 
                '</div>',

            editables: {
                title: {
                    selector: '.cta__title',
                    allowedContent: 'strong em',
                },
                content: {
                    selector: '.cta__text',
                    allowedContent: 'strong em mark br; a[!href]',
                },
                link: {
                    selector: '.cta__link',
                    allowedContent: 'a[!href]',
                }
            },
            
            // No es necesario, se configura en Drupal, pero es bueno mantenerlo por si cambia en el futuro.
            allowedContent:
                'h3(cta__title); div(cta__text, cta__link)',
            
            // No es obligatorio pero permite reconocer widgets en copy & paste.
            upcast: function( element ) {
                return element.name == 'section' && element.hasClass( 'cta__widget' );
            },
        });

        // Imprescindible en Drupal
        editor.ui.addButton('cta_widget', {
            label: 'CTA Widget',
            id: 'cta_widget',
            command: 'cta_widget',
            icon: this.path + 'icons/cta.png'
        });
    }
});

¿Cuándo y por qué es mejor un widget que un paragraph?

La idea principal de este artículo es que los widgets de CKeditor pueden sustituir en algunos casos a paragraphs y fields. Esto puede sonar mal de entrada: ¿por qué prescindir de las capacidades de Drupal 8 para delegarlas en un editor de texto? ¿No existen precisamente los campos o los paragraphs para evitar la tentación de solucionarlo todo en el editor? ¿No introducirá más problemas de mantenimiento a largo plazo de los que elimina en el corto?

Hay que insistir en que solo deberías considerar esta opción en algunos casos muy controlados en los que se cumplan los siguientes requisitos:

  1. La naturaleza del componente a insertar es de totalmente estética o visual, es decir, no depende ni interactúa de ninguna manera con Drupal. 
    • ¿Necesita control de acceso, hacer una referencia a una entidad, acceder a las traducciones, etc.? Olvídate de los widgets.
    • ¿Es una manera bonita o diferente de presentar una información que podría perfectamente mostrarse sin estilado alguno? ¿Es puro front-endEntonces es un buen candidato.
  2. No se prevén grandes cambios en su estructura HTML, al menos en el medio plazo: puede haber cambios de estilado, pero el HTML permanecerá intacto.

Si un componente cumple ambos requisitos, es muy recomendable plantearlo como widget, ya que introduce varias mejoras sobre paragraphs o fields:

  • Evita la sobrecarga de nuestros tipos de contenido con campos y paragraphs innecesarios.
  • Proporciona al usuario editor una verdadera experiencia WYSIWYG en la que poder comprobar en tiempo real cuál es el resultado de su trabajo. Esto, además, suele ser una de las principales quejas que los editores tienen sobre Drupal.
  • Permite un marcado más sencillo e inmediato. Para estilar el HTML y CSS de un paragraph en ocasiones es necesario intervenir a varios niveles, usando varios templates, hooks o field fomatters. Con un widget de CKeditor, el marcado se escribe directamente en un template. En un cierto sentido (repito: en un cierto sentido), se acerca más al paradigma del diseño guiado por componentes (Component-Driven Design) de Vue, React o Angular.
  • Gracias al sistema de Allowed Content Rules de CKEditor, permite definir con granularidad el HTML permitido y prohibido en cada zona editable de un modo más detallado e individualizado de lo que permitirían los campos de Drupal. Ello nos permite darle más poder al editor sin introducir el riesgo de HTML malformado o erróneo.
  • No nos ha dado tiempo a profundizar en ello, pero es posible exponer opciones de configuración del widget usando la API de dialog. De este modo, se podrían exponer opciones como añadir margin o padding, eliminar ciertas zonas editables, cambiar el color de fondo, etcétera.

En resumen, los widgets de CKEditor son una alternativa para algunos tipos de componente que solemos solucionar con herramientas de Drupal. En el caso de componentes puramente visuales, pequeños y con requisitos estables, los widgets aportan muchas ventajas de cara al usuario final y son mucho más fáciles de desarrollar y mantener.