Blog

Tuto : développer une extension Chrome

Tuto : développer une extension Chrome

Publié le mercredi 19 juillet 2017 par Jérôme

A chaque fois que je teste l’interface d’administration d’un site, je me dis qu’il me manque une extension Chrome qui permette de coller du Lorem Ipsum directement dans les champs texte. Il en existe au moins une, mais qui ne permet a priori pas de choisir la longueur du texte collé. Ne sachant absolument pas comment fonctionne une extension, c’était l’occasion idéale pour en apprendre un peu plus en développant la mienne. C'est l'instant Michel Chevalet.

Initialisation du projet

Partons donc du principe que nous voulons une extension qui lorsque le curseur se trouve sur un champ texte, ajoute au menu contextuel de Chrome un sous-menu permettant de coller différentes tailles de texte directement dans le-dit champ: une phrase, un paragraphe court ou un paragraphe long.

Première bonne nouvelle : on trouve énormément de ressources sur le sujet des extensions, et comme toujours chez Google, la documentation officielle est bien fournie. De manière générale, chaque extension est décrite par un fichier manifest, qui contient toutes les informations relatives à son fonctionnement : nom, version, API utilisées, etc.

Initier un projet d’extension est donc assez simple, et ça l’est encore plus avec Yeoman et ce générateur qui offre un environnement de développement optimal : compilation, transpilation ES6, lint, ... Mon extension s'appellera Lorem Pastum, et les lignes de commandes pour la création de cette dernière seront donc les suivantes :

# On installe yeoman, bower et gulp si ce n’est pas déjà le cas
npm install --global yo gulp-cli bower

# On installe le générateur d‘extension
npm install -g generator-chrome-extension

# On crée le répertoire de l’extension et on se place dedans
mkdir lorem-pastum && cd $_

# Enfin on crée l’extension en elle-même
yo chrome-extension

Le générateur va alors vous poser plusieurs questions en plusieurs étapes. La première, un peu déroutante, concerne la stratégie de test utilisée : TDD (Test Driven Development) ou BDD (Behaviour Driven Development). Chacun fait comme il veut, mais dans notre cas assez simple, ce n’est pas déterminant, on laisse le choix par défaut.

Après avoir donné un nom et une description à notre extension, ça se complique un peu.

Il faut savoir qu’une extension Chrome peut concerner 3 couches de développement distinctes :

  • background : c’est ce qui concerne le fonctionnement de l’extension, et qui se fait donc en arrière plan.
  • page : c’est ce qui touche à l’interface même de notre extension. Si vous devez afficher une popup de configuration par exemple, c’est ici que ça se passe.
  • content : c’est ici que l’on va traiter toutes les interactions avec le contenu de la page courante.

Chaque couche sera ensuite contrôlée par des fichiers html et/ou javascript. C’est ce que l’on verra un peu plus tard.

Dans le cas d’une extension qui colle du contenu dans les pages, on a donc besoin de toucher au background (pour créer le menu contextuel et la mécanique consistant à choisir le texte à coller) et au content (pour l’action de coller en elle-même).

Donc à la question “Would you like to use UI actions ?”, la réponse est non, puisque nous n’avons pas de contenu propre à notre extension (du DOM) à afficher dans les pages.

Ensuite, il nous est demandé ”Would you like more UI features ?”. Là, on a clairement besoin de pouvoir interagir avec le contenu des pages. On doit donc choisir “Content scripts”.

On nous demande ensuite les permissions dont a besoin notre extension. Comprenez là qu’il faut lister les API dont nous allons avoir besoin. Ce n’est pas toujours évident de le savoir à l’avance, mais il est toujours possible d’aller modifier le fichier manifest.json ultérieurement. Dans notre cas, seules 2 autorisations sont nécessaires :

  • contextMenus : pour pouvoir ajouter notre propre menu contextuel.
  • clipoardRead : pour récupérer le contenu du presse papier que l’on va utiliser pour modifier la valeur du champ texte sur lequel on se trouve.

Voilà, la base de notre projet est prête. Il n’y a plus qu’à lancer la tâche gulp qui va s’occuper de tout compiler à notre place :

gulp watch

Pour tester notre extension en live, il faut activer le mode développement des extensions (en vous rendant à l’adresse chrome://extensions), cocher la case “Charger l’extension non empaquetée” et sélectionner le répertoire app/ de notre projet.

Si vous suivez ce tuto, vous pourrez même recharger votre extension automatiquement à chaque changement de code.

Développement

Nous pouvons désormais rentrer dans le dur. Pour l’instant, notre application ne fait rien, si ce n’est afficher des messages dans la console Javascript lors de l’installation.

Menu contextuel

Dans un premier temps, occupons-nous d’ajouter notre menu au menu contextuel de Chrome. En suivant les indications de la documentation, on distingue 2 opérations à effectuer :

1/ Ajouter et déclarer l’icône de l’application dans le menu contextuel

Il faut 3 tailles d’icônes différentes : 16x16, 48x48 et 128x128. Plaçons les dans un répertoire images/ de notre projet et déclarons les dans notre fichier manifest.json :

"icons": {
  "16": "images/icon-16.png",
  "48": "images/icon-48.png",
  "128": "images/icon-128.png"
}

2/ Il faut indiquer à Chrome quels vont être les éléments de notre menu

il s’agit d’une tâche relative au background de Chrome, c’est donc dans le fichier background.js (du répertoire app/scripts.babel/, puisque le Javascript est transpilé) que nous allons ajouter le code suivant :

// Ajout du menu contextuel de l'extension
chrome.runtime.onInstalled.addListener((details) => {
    let parentItem = chrome.contextMenus.create({ /* On déclare le menu principal */
    'title': 'Lorem Pastum', /* On lui donne un libellé */
    'id': 'loremPastum', /* On lui donne un identifiant */
    'contexts': ['editable'] /* On liste les contextes dans lesquels notre menu doit s’afficher */
  });
  chrome.contextMenus.create({ /* On déclare un sous-menu */
    'title': 'Coller une phrase',
    'id': 'loremPastum-short', /* On lui donne un identifiant */
    'contexts': ['editable'], /* On précise le contexte d'affichage */
    'parentId': parentItem /* On indique le menu parent */
  });
  chrome.contextMenus.create({
    'title': 'Coller un paragraphe',
    'id': 'loremPastum-medium',
    'contexts': ['editable'],
    'parentId': parentItem
  });
  chrome.contextMenus.create({
    'title': 'Coller un long paragraphe',
    'id': 'loremPastum-large',
    'contexts': ['editable'],
    'parentId': parentItem
  });
});

Rien d’extraordinaire ici, si ce ne sont les paramètres “contexts” qui désignent les contextes dans lesquels notre menu doit s’afficher. Dans la mesure où notre extension ne concerne que les champs texte, il convient de préciser uniquement le contexte “editable”, destiné à cet effet. Les ID vont nous servir à associer du code Javascript à chaque click sur l’un de nos sous-menu. Au niveau du code, ça se concrétise ainsi :

chrome.contextMenus.onClicked.addListener(onClickHandler);
let onClickHandler = (info, tab) => {
  if (info.menuItemId === 'loremPastum-short') {
    sendPasteToContentScript('Lorem ipsum ...');
  }else if (info.menuItemId === 'loremPastum-medium') {
    sendPasteToContentScript('Lorem ipsum dolor sit amet, ...');
  }else if (info.menuItemId === 'loremPastum-large') {
    sendPasteToContentScript('Lorem ipsum dolor sit amet, bla bla bla ...');
  }
}


En fonction de chaque ID, nous allons appeler une fonction sendPasteToContentScript avec un texte différent (volontairement raccourci dans cet exemple).

Envoi du texte vers la page courante

On peut penser alors que le tour est joué et qu’il ne reste qu’à récupérer le contenu de notre champ texte et y coller notre Lorem ipsum. En réalité, c’est un peu plus compliqué.

En effet, le code de la couche background n’a pas accès au DOM de la page courante. Il faut donc trouver une astuce pour envoyer notre texte à la couche content.

Cela peut se faire en utilisant l’API Message, qui comme son nom l’indique permet d’envoyer des messages entre les différentes couches applicatives.

Notre couche background va donc devoir envoyer un message contenant le texte à copier à la couche content. Notre fonction sendPasteToContentScript va donc ressembler à ça :

let sendPasteToContentScript = toBePasted => {
  chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { /* On cherche l’onglet courant */
    chrome.tabs.sendMessage( /* On envoie un message */
      tabs[0].id, /* Au premier tab retourné par la query */
      {type: 'paste', data: toBePasted} /* On définit le type de requête et les données à envoyer */
    );
  });
}

Voilà, notre texte est donc envoyé dans un message à la couche content. Il va maintenant falloir récupérer ce texte et le traiter.

Coller le texte dans le contenu du champ

On y est presque! Le reste du travail se situe donc dans la couche content, autrement dit dans le fichier contentscript.js.

On indique tout d’abord l’action à effectuer lorsqu’un message est reçu :

chrome.runtime.onMessage.addListener((request) => {
  if (request.type === 'paste') {
    insertText(request.data);
  }
});

Ne reste plus qu’à coder la fonction insertText mentionnée ci-dessus et qui va insérer le texte dans le champ texte courant :

let insertText = text => {

  var el = document.activeElement;
  
  let event = new CustomEvent( /* On crée un événement spécifique */
    'pastingLorem', {
      detail: {
      text: text
    },
    bubbles: true,
    cancelable: true
  });
  
  el.addEventListener('pastingLorem', insertTextHandler);
  
  el.focus();
  el.dispatchEvent(event);

  el.removeEventListener('pastingLorem', insertTextHandler);
}

L’astuce pour faire les choses correctement est d’utiliser un Custom Event associé à notre champ texte courant (e.target). Ainsi, l’action de coller le texte pourra être interceptée et utilisée dans le javascript de la page ou dans une autre extension. Concrètement ce n’est pas indispensable, mais c’est mieux.

il ne reste plus qu’à écrire la fonction qui insère concrètement notre texte dans le champ texte passé en paramètre. Rien de particulier au développement d’extension ici, on est sur du pur code Javascript :

let insertTextHandler = (event) => {
  let element = event.target;
  let text = event.detail.text;

  let supportedInputTypes = ['password', 'search', 'text'];
  
  if (element.tagName.toLowerCase() === 'textarea' || inArray(element.type.toLowerCase(), supportedInputTypes)){
  
    let scrollPos = element.scrollTop;
    var caretPos = element.selectionStart;
    
    let front = (element.value).substring(0, caretPos);
    let back = (element.value).substring(element.selectionEnd, element.value.length);
    element.value = front + text + back;
    caretPos = caretPos + text.length;
    element.selectionStart = caretPos;
    element.selectionEnd = caretPos;
    element.focus();
    element.scrollTop = scrollPos;
  
  }else{
    element.value = text;
  }
}

Évidemment, on est sur une extension Chrome, donc on peut utiliser element.selectionStart et element.selectionEnd sans se soucier d’éventuels problèmes de compatibilité avec IE ou autres.

Déploiement

Voilà, notre extension est prête. On peut désormais construire l’application qui sera déposée sur le Chrome Web Store :

gulp build

Le résultat de votre build se trouve donc maintenant dans le répertoire dist/ que nous pouvons dors et déjà zipper.

Rendons nous enfin sur le Tableau de bord du développeur Chrome Web Store. Notons que pour pouvoir déposer notre première application, il faudra débourser la somme mirobolante de 5$.

Il faudra également créer a minima une image de présentation de l’extension (1280x800 ou 640x400) et écrire une jolie description pleine de mots-clés.

Quelques minutes plus tard, notre extension est en ligne. Nous pouvons éventuellement supprimer notre extension de test, installer la version finale et profiter du sentiment de travail accompli.

Pour aller plus loin

Actuellement, l’extension ne fonctionne qu’avec des champs texte classiques. Si vous utilisez des éditeurs de texte avancés comme TinyMCE ou CKEditor, cela ne fonctionnera pas, car le champs texte sont gérés via du code javascript. Ce point devrait être intéressant à travailler, mais demander pas mal d’énergie. Les PR sont les bienvenues sur le git du projet :)

Edit du 20/07 : En fait, en trichant un peu on peut faire fonctionner l'extension assez facilement avec TinyMCE et CKEditor. C'est inclus dans la dernière mise à jour.

Je n’ai pas encore trouvé pourquoi (je n’ai pas beaucoup cherché à vrai dire), mais l’extension semble avoir du mal à cohabiter avec mon bloqueur de pub (uBlock). Certainement un problème de paramètrage.

Il est fortement conseillé d’internationaliser vos extensions. C’est ce que j’ai fait dans la version qui est en ligne sur le Web Store avec une version FR et EN. C’est assez simple à faire si l’on suit rigoureusement la documentation adéquate.

A propos de Jérôme

Développeur web et mobile chez Breizhtorm, amoureux de technologies et de musiques. Sévit de temps à autres sur tropur.com.