Accueil / Blog / Métier / 2014 / Créer facilement un plugin CKEditor 4

Créer facilement un plugin CKEditor 4

Par Yannick Chabbert — publié 30/12/2014
Une API bien fournie et documentée avec des exemples concrets vous permettront de parvenir à la création d'un plugin CKEditor en quelques minutes !
Créer facilement un plugin CKEditor 4

Novice en javascript et de manière générale en éditeur WYSIWYG ? Aucune inquiétude, pas besoin d'être un expert, écrire un plugin CKEditor 4 est vraiment simple et intuitif...

Partons d'un besoin concret : on doit ajouter un bouton dans l'éditeur qui permet de saisir du texte dans une autre langue que la langue du document en cours. Pour cela, une boîte de dialogue s'ouvre et propose à l'utilisateur de sélectionner une langue et de saisir du texte. Ce besoin a été résolu dans les dernières version du cœur de CKEditor, mais c'est pour l'exemple !

CKEditor dialog

La saisie de ce texte sera insérée dans l'éditeur et encapsulée par une balise "span" avec l'attribut "lang" contenant le code ISO de la langue sélectionnée.

CKEditor HTML insert

Si besoin, quand le pointeur est sur la balise "span" ou un de ses enfants, un clic droit ouvrira un menu contextuel. Dans ce menu, il y aura un lien qui au clic ouvrira la même boîte de dialogue avec les champs pré-remplies. La soumission du formulaire modifiera le contenu dans l'éditeur.

CKEditor contextual menu

Le tout sera étonnamment développé en peu de temps et de connaissances.

Dans la documentation de CKEditor, vous devrez lire ce premier didacticiel mais aussi ce second ainsi que la gestion des filtres.

Inutile de s'attarder sur ces pages puisque tout y est très bien expliqué. Après avoir lu ces trois articles, vous avez déjà des bases suffisantes pour créer ce plugin. Et si vous êtes pressé, une simple copie et ré-adaptation du code complet fourni est pratiquement suffisant (il n'est pas totalement identique à celui fourni étape par étape) et surtout, vous passerez à côté de certains concepts importants. En effet, la simple lecture du code est vraiment intuitive même pour un novice...

Pour résumer, l'API de CKEditor vous permet de couvrir des besoins basiques avec seulement quelques déclarations :

  • vous n'avez pas à gérer les dialogues ni même les onglets dans les dialogues ou les formulaires : une simple déclaration structurelle suffit (encapsulées dans des méthodes).
  • récupération des données à partir d'un pointeur, inspection et manipulation du DOM, insertion à partir du pointeur : des méthodes d'API existent pour vous éviter ce travail, jQuery peut donc s'avérer inutile.
  • certaines callback basiques existent déjà (champs requis, etc)
  • gestion de menu contextuel, déclencheurs d'actions notamment avec les boîtes de dialogues associées aux liens du menus : là encore, une simple déclaration est suffisante !
  • gestion des filtrages, exception de surcharge de configuration bloquant votre plugin en deux lignes.

Bref, vous l'aurez compris, inutile d'être un expert javascript pour écrire un plugin CKEditor basique.

Pour reprendre le besoin du plugin cité ci-dessus, voici les sources (le plugin s'intitule "ckeditorLang"). A noter que 90% du code est issu des exemples en liens ci-dessus ; seules quelques adaptations ont été apportées mais le métier reste globalement le même. Par ailleurs, il est adapté pour une utilisation dans Drupal (utilisation de l'objet globale "Drupal". A remplacer donc) :

plugin.js :

(function() {

  // Déclaration du plugin
  CKEDITOR.plugins.add('ckeditorLang',
  {
    // Icône présent dans le répertoire "icons" à la racine du plugin
    icons: 'ckeditorLang',
    init : function(editor)
    {

      // Déclaration d'une boîte de dialogue, avec en prime:
      // - la déclaration d'ajout d'un contenu HTML inséré ainsi que les attributs rattachés (advanced filters)
     //  - la désactivation du plugin (bouton non visible) si une surcharge de la configuration exclue les balises span avec un attribut lang
      editor.addCommand('ckeditorLangDialog', new CKEDITOR.dialogCommand('ckeditorLangDialog', {
        allowedContent: 'span[lang]',
        requiredContent: 'span[lang]'
      }));

      // Ajout d'un bouton dans une zone prédéfinie par défaut avec l'action associée (boîte de dialogue)
      editor.ui.addButton('ckeditorLang', {
        label : Drupal.t( 'Insert text in a specific language' ),
        command : 'ckeditorLangDialog',
        toolbar: 'insert'
      });

      // Déclaration du fichier contenant la boîte de dialogue
      CKEDITOR.dialog.add('ckeditorLangDialog', this.path + 'dialogs/ckeditorLang.js');

      if (editor.contextMenu) {

        // Ajout d'un group de menu contextuel, avec un lien associé à une commande (dialogue)
        editor.addMenuGroup('ckeditorLangGroup');
        editor.addMenuItem('ckeditorLangItem', {
          label: Drupal.t('Edit translation'),
          icon: this.path + 'icons/ckeditorLang.png',
          command: 'ckeditorLangDialog',
          group: 'ckeditorLangGroup'
        });

        // Ajout d'un context d'affichage du lien du groupe de menu
        // si l'élément pointé est une balise span ou un de ses parents.
        // A noter qu'il n'est pas possible d'avoir une sélection plus spécifique ("lang" attribut).
        // Néanmoins, il est possible d'introduire des conditions ainsi que la position
        // des éléments dans le DOM.
        // Voir : http://docs.cksource.com/ckeditor_api/symbols/CKEDITOR.dom.node.html#getAscendant
        editor.contextMenu.addListener(function(element) {
          if (element.getAscendant('span', true)) {
            return {
              ckeditorLangItem: CKEDITOR.TRISTATE_OFF
            };
          }
        });
      }
    },
  });
} )();

ckeditorLang.js (dialog) :

 (function() {

  // Déclaration de la boîte de dialogue
  CKEDITOR.dialog.add('ckeditorLangDialog', function (editor) {
    return {
      title: Drupal.t('Specific language text'),
      minWidth: 400,
      minHeight: 200,

      // Contenu de la boîte de dialogue avec une tab, contenant des éléments de formulaire.
      contents: [
        {
          id: 'tab',
          label: Drupal.t('Specific language text'),
          elements: [
            // Liste de sélection, http://docs.ckeditor.com/#!/api/CKEDITOR.dialog.definition.select
            {
              type: 'select',
              id: 'language',
              label: Drupal.t('Language'),
              items: Drupal.settings.ckeditor_lang.language_options,
              // Appellé à l'ouverture de la boîte de dialogue via la méthode setupContent() dans la méthode onShow()
              setup: function(element) {
                // Mise à jour de la valeur de l'élément du formulaire avec l'attribut de l'élément
                // copié par référence dans la méthode onShow().
                this.setValue(element.getAttribute("lang"));
              },
              // Appellé à la soumission du formulaire par la méthode commitContent() de la dialog dans la méthode onOk().
              commit: function(element) {
                // Ajout/modification d'un attribut lang à l'élément avec la valeur du champs
                element.setAttribute("lang", this.getValue());
              }
            },
            // Textarea : http://docs.ckeditor.com/#!/api/CKEDITOR.dialog.definition.textarea
            {
              type: 'textarea',
              id: 'text',
              label: Drupal.t('Text'),
              // Callback de vérification
              validate : CKEDITOR.dialog.validate.notEmpty(Drupal.t('Text field cannot be empty')),
              setup: function(element) {
                this.setValue(element.getText());
              },
              commit: function(element) {
                element.setText(this.getValue());
              }
            }
          ]
        }
      ],
      // Callback appelée à l'ouverture de la boîte de dialogue
      onShow: function() {

        // Récupération de la sélection dans l'éditeur et de son premier élément
        var selection = editor.getSelection(),
            element = selection.getStartElement();

        // Si un élément existe, on récupère l'élément span (lui-même ou un de ses parents)
        if (element) {
          element = element.getAscendant('span', true);
        }

        // Si on a pas d'élément ou que ca n'est pas un span, alors on
        // considère que l'on est dans un cas d'insertion
        if (!element || element.getName() !== 'span') {
          // Création d'un élément span.
          element = editor.document.createElement('span');
          this.insertMode = true;
        }
        else {
          this.insertMode = false;
        }

        // Copie par référence dans l'instance pour une manipulation ultérieure.
        this.element = element;

        // Si ca n'est pas une insertion mais une modification, on appelle les méthodes
        // setup() des éléments du formulaire. Donc si c'est une insertion, les
        // valeurs par défaut seront utilisées.
        if (!this.insertMode) {
          this.setupContent(element);
        }
      },
      // Callback appelée à la soumission du formulaire (validé)
      onOk: function() {

        var dialog = this,
            span = this.element;

        // Appel des méthodes commit de chacun des éléments du formulaire.
        // L'élément crée ou précédemment sélectionné est mise a jour (son texte
        // et son attribut lang).
        dialog.commitContent(span);

        // Insertion de l'élément dans le DOM à la position du pointeur. Sinon,
        // il est déjà mis à jour par le commit (modification).
        if (dialog.insertMode) {
          editor.insertElement(span);
        }
      }
    };
  });
})();

C'est tout ! Votre plugin est désormais opérationnel. On verra dans un prochain article comment s'intégrer aux modules Drupal CKEditor et WYSIWYG.

ABONNEZ-VOUS À LA NEWSLETTER !
Voir aussi
À Makina, la JS fatigue n'existe pas... 01/04/2019

...car la passion l'emporte

Mon Top 30 des modules Drupal 8 Mon Top 30 des modules Drupal 8 16/02/2019

Transcription d'une conférence donnée au Drupalcamp Paris 2019

Makina Corpus lance une nouvelle offre pour sécuriser les projets Drupal des ESN Makina Corpus lance une nouvelle offre pour sécuriser les projets Drupal des ESN 18/02/2019

Formations et accompagnement pour sécuriser les projets Drupal

Drupal : un CMS pas comme les autres Drupal : un CMS pas comme les autres 15/02/2019

Mais moi je l'aime, c'est pas de ma faute…

Mise en pratique de RxJS dans Angular Mise en pratique de RxJS dans Angular 13/08/2018

Les quelques bases suffisantes pour bien utiliser RxJS dans Angular. Cet article a été écrit ...