L'éditeur visuel de WordpressOn pourra dire tout le mal que l’on voudra de Wordpress, et souvent à juste titre, mais on ne pourra nier l’impressionnant travail accompli sur l’éditeur. Partis d’un simple textarea, ils sont peu à peu arrivé à un éditeur hybride WYSIWYG / HTML adossé à une impressionnante, quoique perfectible bibliothèque de média. La possibilité de changer d’éditeur en cours d’édition, sans recharger la page ni perdre son contenu est aujourd’hui ce que je préfère dans le blogware le plus célèbre au monde.
C’est d’ailleurs ce que je vous propose aujourd’hui : la création pas à pas d’un tel éditeur, mais pour les applications utilisant le framework Ruby on Rails. Nous nous baserons pour cela sur l’éditeur riche FCKEditor, et sur la librairie Javascript Quicktags d’Alex King, également utilisée par Wordpress.

Téléchargez et Installez FCKEditor pour Rails

J’avais d’abord pensé partir de la création d’une mini application Rails pour étayer mon propos. Je considère finalement que vous savez faire, ou en tout cas, que vous trouverez ailleurs. On appelle ça la flemme.

Téléchargez la dernière version du plugin FCKEditor pour Ruby on Rails. Décompressez le dans le répertoire vendor/plugins de votre application. Installez maintenant le plugin à l’aide de la tâche rake dédiée :

$ rake fckeditor:install

Éditez le layout de votre application. Sauf cas exceptionnel, il se trouve dans app/views/layouts/. Dans la partie head, ajoutez la ligne suivante :

<%= javascript_include_tag :fckeditor %>

Téléchargez et Installez la librairie Quicktags

Téléchargez maintenant le fichier source quicktags.js. Décompressez l’archive, puis, copiez le dans le répertoire contenant les fichiers Javascript de votre application.

$ cd chemin/vers/mon/application/public/javascripts
$ curl -O http://alexking.org/projects/js-quicktags/download/js-quicktags.zip
$ unzip js-quicktags.zip
$ mv js-quicktags/js_quicktags.js quicktags.js

Éditez à nouveau le layout de votre application. Insérez la ligne suivante juste en dessous de celle que vous avez précédemment ajoutée :

<%= javascript_include_tag 'quicktags' %>

Permettez à vos utilisateurs de sauvegarder leur éditeur favori

Vous allez maintenant donner la possibilité à vos utilisateurs de sauvegarder leur éditeur favori dans leur profil. Vous utiliserez cette nouvelle fonctionnalité lors des changements à la volée. Pour cela, il vous faudra modifier quelque peu votre modèle, afin d’y ajouter un champs editor de type string. Ce dernier aura deux valeurs possibles.

  • simple : l’éditeur HTML standard.
  • visual : FCKEditor.

Pour cela, créez une nouvelle migration dans db/migrate. Celle-ci ajoutera une nouvelle colonne editor à votre table users, et forcera l’utilisation de l’éditeur HTML pour tous vos utilisateurs dans un premier temps.

class AddEditorToUser < ActiveRecord::Migration
  def self.up
    add_column :users, :editor, :string, :default => 'simple'
    
    unless $schema_generator
      users = User.find(:all)
      users.each do |user|
        user.editor = 'simple'
        user.save!
      end
    end
  end

  def self.down
    remove_column :users, :editor
  end
end

Lancez maintenant la migration proprement dite :

$ rake db:migrate

Durant tout le reste de ce didacticiel, vous manipulerez un objet current_user. Il s’agit en fait de l’objet créé lors de l’authentification de votre utilisateur.

Insérez l’éditeur dans votre formulaire de saisie

Dans votre formulaire de saisie, remplacez l’appel à votre textarea par :

<div id='editor'>
  <div id="quicktags" style='<%= "display: none;" if current_user.editor == 'visual' %>'>
    <%= javascript_tag "edToolbar('article_body')" %>
  </div>
  <div id='edition'>
    <% if current_user.editor == 'visual' -%>
      <%= fckeditor_textarea('article', 'body', {:height => '300', :class => 'medium'}) -%>
    <% else -%>
      <%= text_area('article', 'body', {:height => '300', :class => 'medium'}) -%>
    <% end -%>
  </div>  
</div>

Le comportement des Quicktags vous oblige à les charger quelque soit l’éditeur sélectionné par vous utilisateur. Il est donc nécessaire de le cacher dans le cas où l’utilisateur a choisi l’éditeur visuel.

Insérez les liens de bascule

Vous allez maintenant ajouter le changement d’éditeur à la volée. Vous allez envoyer un appel AJAX qui lancera une méthode de contrôleur. Celle-ci changera si besoin l’éditeur par défaut dans le profil de l’utilisateur, puis chargera une vue qui :

  1. Récupérera le contenu de l’éditeur en cours, s’il y en a.
  2. Affichera le nouvel éditeur.
  3. Placera dans ce dernier le contenu précédemment récupéré.

Ajoutez le code suivant au dessus de l’éditeur mis en place plus tôt :

<div id="editor-menu">
  <span id="f" class="<%= 'active' if current_user.editor == 'visual' %>">
    <%= link_to_remote("Éditeur Visuel", 
            :url => { :action => 'create_fck_editor' }, 
            :loading => "new Element.show('update_spinner_fck')",
            :success => "new Element.toggle('update_spinner_fck')",
            :update => 'edition') %>
    <%= image_tag("spinner-blue.gif", :id => "update_spinner_fck", :style => 'display:none;') %>
  </span>
  <span id="s" class="<%= 'active' if current_user.editor == 'simple' %>">
    <%= link_to_remote("Éditeur HTML", 
            :url => { :action => 'create_simple_editor' }, 
            :loading => "new Element.show('update_spinner_simple')",
            :success => "new Element.toggle('update_spinner_simple')",
            :update => 'edition') %>
    <%= image_tag("spinner-blue.gif", :id => "update_spinner_simple", :style => 'display:none;') %>
  </span>
</div>

Votre formulaire devrait maintenant ressembler à :

<div id="editor">
  <div id="editor-menu">
    <span id="f" class="<%= 'active' if current_user.editor == 'visual' %>">
      <%= link_to_remote("Éditeur Visuel", 
              :url => { :action => 'create_fck_editor' }, 
              :loading => "new Element.show('update_spinner_fck')",
              :success => "new Element.toggle('update_spinner_fck')",
              :update => 'editor') %>
      <%= image_tag("spinner-blue.gif", :id => "update_spinner_fck", :style => 'display:none;') %>
    </span>
    <span id="s" class="<%= 'active' if current_user.editor == 'simple' %>">
      <%= link_to_remote("Éditeur HTML", 
              :url => { :action => 'create_simple_editor' }, 
              :loading => "new Element.show('update_spinner_simple')",
              :success => "new Element.toggle('update_spinner_simple')",
              :update => 'editor') %>
      <%= image_tag("spinner-blue.gif", :id => "update_spinner_simple", :style => 'display:none;') %>
    </span>
  </div>
  <div id="quicktags" style='<%= "display: none;" if current_user.editor == 'visual' %>'>
  <%= javascript_tag "edToolbar('article_body_and_extended')" %>
  </div>
  <div id="edition">
    <% if current_user.editor == 'visual' -%>
      <%= fckeditor_textarea('article', 'body', {:height => '300', :class => 'medium'}) -%>
    <% else -%>
      <%= text_area('article', 'body', {:height => '300', :class => 'medium'}) -%>
    <% end -%>
  </div>
</div>

Linktoremote est un des helper standard de Rails qui a probablement le plus fait pour la notoriété du framework. Il permet en effet de générer un lien effectuant un appel AJAX. Ici, vous l’utilisez avec les paramètres suivants :

  • Un libellé, spécifiant l’éditeur à afficher.
  • Une URL, en l’occurrence celle de l’action de création de l’éditeur dans votre contrôleur.
  • Loading : un événement à lancer pendant le chargement du lien.
  • Success : action à effecuer quand le chargement est terminé.
  • Update : le conteneur HTML à modifier avec le résultat de l’appel AJAX.

Le petit détail qui change tout : l’apparition du spinner pendant le chargement de l’éditeur.

Créez les méthodes de contrôleur

Il vous faut maintenant ajouter deux méthodes au contrôleurs en cours. La première gère le passage à l’éditeur simple, la seconde à l’éditeur visuel. Toutes deux enregistreront également l’éditeur choisi dans les préférences de l’utilisateur.

def create_fck_editor
  current_user.editor = 'visual'
  current_user.save!
  render :partial => "fck_editor"
end

def create_simple_editor
  current_user.editor = 'simple'
  current_user.save!
  render :partial => "simple_editor"
end

Ajoutez les vues manquantes

Il vous manque maintenant les vues appelées par les méthodes précédentes. Il vous faut utiliser deux partial. Le premier récupère le contenu de l’éditeur simple, charge l’éditeur visuel, et y transfert le contenu précédemment récupéré. Le second fait exactement l’inverse.

_fck_editor.html.erb
<script type="text/javascript">
  $('quicktags').style.display = 'none';
  $('s').className = 'inactive';
  $('f').className = 'active';
  
  function loadContent()
  {
    content = $('content_body').innerHTML;
    // Le nom de l'instance change en fonction de votre formulaire
    var oEditor = FCKeditorAPI.GetInstance('content__body__fckeditor') ;
    oEditor.SetHTML(content);
  }
  
  function FCKeditor_OnComplete( editorInstance )
  {
      editorInstance.Events.AttachEvent( 'OnAfterSetHTML', loadContent ) ;
  }
  
</script>

<%= fckeditor_textarea('article', 'body', {:height => '300', :class => 'medium'}) %>
_simple_editor.html.erb
<script type="text/javascript">
  $('quicktags').style.display = 'block';
  $('s').className = 'active';
  $('f').className = 'inactive';
</script>
<%= text_area('article', 'body', {:height => '300', :class => 'medium'}) %>

Il ne vous manque plus qu’un semblant de CSS afin de mettre tout cela en forme.

#editor {
  width: 700px;
}

#edition {
  background: #464646;
  padding: 10px;
}

#quicktags {
  background: #464646;
  padding-top: 10px;
  padding-left: 10px;
}

#editor-menu {
  text-align: right;
}

#editor-menu span.active {
  background: #464646;
  padding: 5px;
}

#editor-menu span.inactive {
  background: #f9f9f9;
  padding: 0px;
}

Et maintenant… le refactoring

Bon, ça marche, et j’en entends me dire que c’est déjà pas mal. Honnêtement, approche pas à pas oblige, vous avez volontairement travaillé avec un code franchement crade.

  • Le code est hyper spécifique à votre formulaire. Vous ne pourrez donc pas le réutiliser ailleurs sans devoir le dupliquer.
  • Ce qui tombe bien, puisque de toutes manières, la lecture du code contenu dans les membres des blocs if … else … est exactement la même à trois noms de variables près.

Comme disait Eric, mon formateur en TDD, en se tapotant le bout du nez, quand je vois du code dupliqué, je sors mon revolver je sais que ça ne sent pas bon. Et dans ce cas, une seule solution la sodomisation la refactorisation.

Appelez les sources de FCKEditor et des quicktags

Au lieu de deux appels au helper javascriptincludetag, vous n’allez plus en faire qu’un seul.

<%= javascript_include_tag :fckeditor, 'quicktags' %>
Créez un helper afin d’insérer l’éditeur en fonction des préférences de l’utilisateur
def t_textarea(object_name, method, options)
  return fckeditor_textarea(object_name, method, options) if current_user.editor == 'visual'
    
  return text_area(object_name, method, options)
end

Classe non ?

Profitez-en pour créer un helper qui affichera les Quicktags, en prenant en paramètre un conteneur et un textarea :

def display_quicktags(container, textarea)
  html = "<div id='#{container}' "
  html << "style='display: none;'" if current_user.editor == 'visual'
  html << ">"
  html << javascript_tag "edToolbar('#{textarea}')"
  html << "</div>"
end

Next step, vos deux linktoremote, qui méritent eux aussi d’être simplifiés.

# Editor prend les valeurs simple ou visual
def build_editor_link(label, action, id, update, editor)
  link = link_to_remote(label, 
          :url => { :action => action, 'editor' => editor }, 
          :loading => "new Element.show('update_spinner_#{id}')",
          :success => "new Element.toggle('update_spinner_#{id}')",
          :update => "#{update}")
  link << image_tag("spinner-blue.gif", :id => "update_spinner_#{id}", :style => 'display:none;')
end

La magie du refactoring (par le vide) rend votre formulaire franchement imbitable tout de suite plus lisible :

<div id="editor">
  <div id="editor-menu">
    <span id="f" class="<%= 'active' if current_user.editor == 'visual' %>">
      <%= build_editor_link("Éditeur Visuel", 'set_editor' 'edition', 'fck', 'fck') %>
    </span>
    <span id="s" class="<%= 'active' if current_user.editor == 'simple' %>">
      <%= build_editor_link("Éditeur HTML", 'set_editor' 'edition', 'simple', 'simple') %>
    </span>
  </div>
  <div id="edition">
    <%= display_quicktags('quicktags', 'article_body') %>
    <%= t_textarea('article', 'body', {:height => '300', :class => 'medium'})) %>
  </div>
</div>
Regroupez les deux méthodes du contrôleur en une seule

Si vos deux méthodes de contrôleur font quasiment la même chose, pourquoi ne pas les regrouper en une seule ? C’est une très bonne question et je ne vous remercie pas de me l’avoir posée. vous allez donc les remplacer par une méthode seteditor, qui prendra en paramètre le nom de l’éditeur envoyé par votre helper buildeditor_link.

def set_editor
  # Note : je ne suis pas certain de ma regexp, je refactorise mon code 
  # pondu hier soir entre une et deux heures du matin avec un bébé hurlant 
  # sur mes genoux pour cause de rage de dents pendant que j'écris 
  # cet article.

  return unless params[:editor] =~ /^visual|simple$/
  current_user.editor = params[:editor].to_s
  current_user.save!
  render :partial => "#{params[:editor].to_s}_editor"
end

Mieux non ? Ils ne vous reste plus qu’à renommer fckeditor.html.erb en visualeditor.html.erb, et le tour est joué.

Évidemment, tout ceci avait un but, intégrer un tel éditeur à Typo, le blogware en Ruby on Rails bien connu. C’est fait depuis cette nuit, et vous pouvez le tester sur notre plate-forme de démonstration, login admin, password admin.
Si Eric était là, il me casserait certainement les doigts à grands coups de pelle rouillée. Et pour cause, chacune de nos opérations de refactoring aurait du être précédée d’une séance d’écriture de tests unitaires visant à valider que le code fait bien ce qu’on attend de lui. Puisque je vous ai mâché le travail en vous le fournissant, vous pouvez toujours les écrire à ma place. Merci.

Perry the Platypus wants you to subscribe now! Even if you don't visit my site on a regular basis, you can get the latest posts delivered to you for free via Email: