Développer un plugin pour WordPress
4 – Création de l’interface du plugin tinyMCE
20 septembre 2016.
Premiers pas
Au chapitre précédent, nous avons établi les fondations de notre extension en définissant ses options et en donnant la possibilité à nos utilisateurs de les modifier au gré de leurs envies du moment.
Il est maintenant temps de nous attaquer à l’interface que nous allons leur proposer pour ajuster au sein de l’éditeur de WordPress les paramètres propres à une image.
Mais avant cela, pour ceux qui l’ignoreraient encore, il est bon de préciser que ledit éditeur comporte deux modes d’écriture :
- Le premier, dit Visuel, repose sur tinyMCE. Il dispose d’une barre d’outils censés simplifier la mise en forme et tente autant que faire se peut de se rapprocher du principe WYSIWYG en dissimulant soigneusement au rédacteur le coté abscons et les contraintes habituelles du codage HTML. C’est avec lui que nous allons tenter de coopérer…
- Le second, dit Texte, est autrement plus spartiate et l’écriture s’y effectue peu ou prou sous la forme d’un code HTML traditionnel.
À noter en passant qu’il n’est pas rare (et c’est une litote !) que le basculement en brute d’un mode à l’autre de l’éditeur génère une palanquée de problèmes et une désorganisation radicale de la mise en page… Nous veillerons à ne pas tomber dans ce piège.
Bref, en ce qui nous concerne et puisque notre extension est destinée à rendre la vie de nos utilisateurs plus rieuse en s’intégrant harmonieusement au mode Visuel de l’éditeur, il nous faut parvenir à la faire dialoguer avec tinyMCE, autrement dit, construire un plugin tinyMCE au cœur d’un plugin WordPress.
TinyMCE, c’est du javascript. Et donc, cela ne vous étonnera point, un plugin tinyMCE, c’est… du javascript aussi. Forcément !
Rendons-nous donc dans le répertoire admin/js pour y créer un fichier text-floating-image-tinyMCE.js réduit à sa plus simple expression :
0 1 2 3 4 5 6 7 8 9 10 11 12 |
(function() { 'use strict'; //Enregistrement du plugin auprès de tinymce tinymce.PluginManager.add('textFloatingImageTinyMCE', function(editor, url) { /* * editor est une référence à l'éditeur de tinyMCE avec lequel on interagit * url est le lien de notre fichier */ alert('Bonjour ! Je serai bientôt un super plugin tinyMCE !!!'); }); })(); |
Ceci fait et vite fait, il nous faut signaler à WordPress l’existence de notre plugin.
Comme d’habitude, l’opération s’effectue en deux temps : tout d’abord, une fonction add_tinymce_plugin() est définie dans la classe dédiée admin/class-text-floating-image-admin.php. Elle a la redoutable charge d’expliquer à WordPress que « hello ! coucou ! y’a un nouveau super plugin pour ton éditeur à toi que t’as que je t’invite à rencontrer là maintenant pas plus tard que tout de suite ».
217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 |
/** * Déclaration du plugin tinyMCE * La fonction est appelée sur le hook mce_external_plugins * dans la fonction define_admin_hooks() de includes/class-text-floating-image.php * * @since 1.0.0 */ function add_tinymce_plugin($plugin_array) { /* * textFloatingImageTinyMCE est le petit nom que nous donnons à notre plugin tinyMCE. * Ce nom ne doit comporter ni espace ni tiret. * La ligne qui suit l'ajoute au tableau des plugins de sieur tinyMCE * en pointant vers le fichier js qui contient toute la logique de la bête */ $plugin_array['textFloatingImageTinyMCE'] = plugin_dir_url(__FILE__).'js/text-floating-image-tinyMCE.js'; return $plugin_array; } |
Elle est ensuite accrochée (ben vi ! hook = crochet) à l’évènement mce_external_plugins dans la fonction define_admin_hooks() de notre classe dirigeante includes/class-text-floating-image.php.
198 199 200 201 202 203 204 205 206 |
/** * Déclaration du plugin tinyMCE * La fonction add_tinymce_plugin figure dans la classe * admin/class-text-floating-image-admin.php. * Documentation : https://codex.wordpress.org/Plugin_API/Filter_Reference/mce_external_plugins */ $this->loader->add_filter('mce_external_plugins', $plugin_admin, 'add_tinymce_plugin'); |
Si vous vous rendez maintenant dans l’éditeur visuel de votre installation WordPress, vous devriez voir surgir à l’initialisation une joyeuse et pimpante boite de dialogue telle que nous l’avons définie dans admin/js/text-floating-image-tinyMCE.js.
Ajout d’un bouton dans la barre d’outils de tinyMCE
Pour offrir un accès à notre future fenêtre de paramétrage, nous devons bien évidemment nous coltiner l’ajout d’un bouton dans la barre d’outils de tinyMCE.
Qui dit bouton, dit icône…
Et qui dit icône, dit image…
Et qui dit mieux ne dit pas moins…
Enfin bref ! Voici celle que j’ai construite de mes petites mains à moi par l’entremise d’un programme de dessin vectoriel d’une société d’édition plutôt connue…
Bon ! Mes proches sont d’ores et déjà au courant et vous l’êtes désormais vous aussi : je n’ai aucun talent graphique et un gout de chiottes (n’empêche que ce serait charitable à vous que d’en finir avec ce sourire sardonique que je devine sur votre visage moqueur malgré les écrans qui nous séparent).
Nonobstant, la chose réduite à quelques 20 pixels devrait malgré tout faire l’affaire pour une bête icone (téléchargeable ici pour celles et ceusses qui ne sont pas dégoûtés).
Le boilerplate sur lequel nous avons choisi de nous reposer ne prévoit pas de tiroir où ranger les images. Rien ne nous empêche cependant de suppléer cette absence comme des grandes personnes que nous sommes. Glissons donc sans plus de retenue cette foutue icône dans un dossier img que nous créons à la racine du répertoire admin.
À première vue, il semblerait qu’un petit tour dans l’austère documentation de tinyMCE suffise à nous donner la marche à suivre quant à l’insertion de notre bouton dans la barre d’outils. Selon cette source, nous pourrions nous contenter pour atteindre notre but de quelque chose du genre :
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
(function() { 'use strict'; //Enregistrement du plugin auprès de tinymce tinymce.PluginManager.add('textFloatingImageTinyMCE', function(editor, url) { /* * editor est une référence à l'éditeur de tinyMCE avec lequel on interagit * url est le lien de notre fichier */ /* http://archive.tinymce.com/wiki.php/api4:method.tinymce.Editor.addButton */ editor.addButton('textFloatingImageTinyMCE', { title: 'Aligner verticalement texte & image', //L'image est rangée dans : //repertoire_Wordpress/wp-content/plugins/text-floating-image/admin/img //url vaut : //repertoire_Wordpress/wp-content/plugins/text-floating-image/admin/js //On retire les deux derniers caractères de url et on y ajoute 'img/icon.png' //pour obtenir l'adresse de l'image : //repertoire_Wordpress/wp-content/plugins/text-floating-image/admin/img/icon.png image: url.substr(0, url.length-2) + 'img/icon.png', onclick: buttonClicked }); function buttonClicked() { alert("Bientôt s'affichera sur votre écran une splendide fenêtre de paramétrage !!!"); } }); })(); |
Vous constaterez par vous même avec la déception qui va avec qu’on dirait bien que cela ne suffit pas franchement. On a beau cherché, écarquillé les mirettes, sonder, creuser, fouiner, fouiller, fourrager et tout et tout, aucune trace de notre bouton chéri à nous nulle part.
Faut dire que WordPress est parfois du genre pointilleux rond-de-cuir et n’apprécie pas forcément qu’on tente comme ça d’introduire dans son dos des trucs qu’il n’aurait pas validé de par lui-même. Le fait est que, tout à la fois pour le satisfaire et parvenir à nos fins, il nous faut encore une fois passer par la voie hook. En même temps, au point où nous en sommes, nous n’allons pas rechigner pour si peu d’autant que la procédure n’a (presque) plus de secret pour nous.
Et d’un rapide petit insert dans la fonction define_admin_hooks() de includes/class-text-floating-image.php :
206 207 208 209 210 211 212 213 214 215 216 |
/** * Ajout du bouton dans la barre d'outils de tinyMCE * La fonction add_tinymce_toolbar_button est logée dans la classe * admin/class-text-floating-image-admin.php. * Documentation : * https://developer.wordpress.org/reference/hooks/mce_buttons/ * https://codex.wordpress.org/TinyMCE_Custom_Buttons */ $this->loader->add_filter('mce_buttons', $plugin_admin, 'add_tinymce_toolbar_button'); |
Et d’un autre dans admin/class-text-floating-image-admin.php :
242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 |
/** * Ajout du bouton dans la barre d'outils de tinyMCE * La fonction est appelée sur le hook mce_buttons * dans la fonction define_admin_hooks() de includes/class-text-floating-image.php * * @param array $buttons : tableau des boutons enregistrés dans tinyMCE * @return array : le tableau original augmenté de notre bouton * @since 1.0.0 */ function add_tinymce_toolbar_button($buttons) { array_push( $buttons, 'textFloatingImageTinyMCE' ); return $buttons; } |
Eh ben voilà ! On l’a notre bouton !
Euh… Je ne voudrais pas froidement doucher votre enthousiasme mais, maintenant qu’il est là et bien là, il ne nous reste plus qu’à l’améliorer : nous nous sommes en effet interdit de rendre notre plugin accessible aux images qui ne respectent pas les quatre règles édictées ici. Face à ce genre de situation, il sera donc judicieux de désactiver notre bouton. Par ailleurs, puisque les boutons de la barre d’outils sont dotés d’un état dit active (ils sont alors agrémentés d’une fine bordure grise), nous en profiterons pour attribuer cet état à notre bouton dès lors qu’une image d’ores et déjà traitée par notre extension sera sélectionnée. Dans ce cas précis, nous prendrons soin par ailleurs de désactiver les boutons qui gèrent l’alignement.
Les moins attentifs d’entre vous ne manqueront pas de se demander comment on pourrait deviner qu’une image est ou n’est pas affectée par notre plugin. Ceux qui suivent leur expliqueront doctement que, comme évoqué ici-même, dès lors que notre extension est appliquée à une image, ladite image se trouve pourvue d’un attribut data-text_floating_image porteur d’un identifiant unique. Et que donc réciproquement, si une image porte l’attribut data-text_floating_image, c’est qu’elle est prise en charge par notre plugin.
Pour mener à bien notre présente et délicate mission, nous utiliserons l’événement onPostRender, qui tel le cri d’effroi poussé par le nouveau né lorsqu’il découvre notre monde, est éructé par notre bouton lui-même aussitôt qu’il en a fini d’apparaître. Cet événement va nous permettre d’obtenir une référence audit bouton via this, référence que nous nous empresserons d’affecter à une variable que nous pourrons invoquer tout à loisir et à tout moment par ailleurs. Et tout particulièrement dans notre réponse à l’événement NodeChange de l’éditeur, lequel se produit à chaque nouvelle sélection. Oui, c’est totalement tordu mais c’est aussi le seul moyen que j’ai pu trouver pour réaliser la chose et autant dire que je ne l’ai pas déniché tout seul (voir par exemple ici et là ou bien encore là).
Retour donc dans admin/js/text-floating-image-tinyMCE.js :
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 |
(function() { 'use strict'; //Enregistrement du plugin auprès de tinymce tinymce.PluginManager.add('textFloatingImageTinyMCE', function(editor, url) { /* * editor est une référence à l'éditeur de tinyMCE avec lequel on interagit * url est le lien de notre fichier */ //DÉCLARATION DES VARIABLES var dom,//référence au dom de l'éditeur. Initialisée dans l'événement init de l'éditeur selection,//L'objet selection de l'éditeur. Initialisée dans l'événement init de l'éditeur img,//L'image sélectionnée. Initialisée dans l'événement NodeChange //dès lors que l'élément sélectionné est une image imgContainer,//Le bloc contenant l'image. Initialisée dans l'événement NodeChange //dès lors que l'élément sélectionné est une image button,//Le bouton de notre extension ajouté à la barre d'outils de tinyMCE alignLeftButton, alignCenterButton, alignRightButton, //Une référence aux trois boutons d'alignement pour pouvoir gérer leur état //Toutes les variables boutons sont initialisées dans la fonction addButton prevEl;//une variable pour gérer plus judicieusement l'événement NodeChange //qui se déclenche deux fois editor.on('init', function(e) { dom = editor.dom; selection = editor.selection; }); /***************************************************************************** GESTION DE L'ÉTAT DES BOUTONS DE LA BARRE D'OUTILS EN FONCTION DE LA SÉLECTION ****************************************************************************** On s'appuie sur l'évènement "NodeChange" de l'éditeur. http://archive.tinymce.com/wiki.php/api4:event.tinymce.Editor.NodeChange */ editor.on('NodeChange', function() { //On stocke l'élément sélectionné dans l'éditeur //http://archive.tinymce.com/wiki.php/api4:method.tinymce.dom.Selection.getNode var el = selection.getNode(); //L'événement "NodeChange" a tendance a se déclencher deux fois. //Pour alléger la charge de travail, on n'exécute notre tâche que si l'on constate //par nous-même que l'élément actuellement sélectionné est bien différent //de l'élément précédemment sélectionné, lequel est stocké dans la variable prevEL if(el != prevEl) { prevEl = el; //Reset de l'état des boutons button.disabled(true); button.active(false); alignLeftButton.disabled(false); alignCenterButton.disabled(false); alignRightButton.disabled(false); //Si l'élément sélectionné est une image if (el.nodeName == 'IMG') { img = el; //Initialisation de la variable imgContainer imgContainer = dom.getParent(img,dom.isBlock); //Si l'extension est d'ores et déjà appliquée à l'image if (img.hasAttribute('data-text_floating_image')) { button.disabled(false); button.active(true); //On désactive les boutons d'alignement alignLeftButton.disabled(true); alignCenterButton.disabled(true); alignRightButton.disabled(true); //Sinon et si l'image n'est pas incluse dans un élément li } else if (imgContainer.nodeName != 'LI') { //On s'assure avant tout, qu'au sein même de son block container, //l'image n'est pas suivie d'une autre image. //Pour ce faire, on convertit en tableau la collection HTML //des images contenues dans le container //et on vérifie que l'image sélectionnée est bien le dernier élément //de ce tableau //http://stackoverflow.com/a/222847 var arr = [].slice.call(imgContainer.getElementsByTagName('img')); if (arr.indexOf(img) == arr.length - 1) { //Si l'image est bien la dernière de son block container, //on recherche la présence de texte après l'image //au sein de son container //et, dès lors qu'on en trouve, on active le bouton //http://stackoverflow.com/a/10730777 var n, a = [], walk = editor.getDoc().createTreeWalker(imgContainer, NodeFilter.SHOW_ALL, null, false); //On construit un tableau des noeuds contenus dans le container while(n = walk.nextNode()) a.push(n); //On réduit ce tableau aux noeuds qui suivent l'image a.splice(0, a.indexOf(img) + 1); //Dès qu'on détecte un noeud contenant du texte, //on active le bouton et on passe à la suite for(var i = 0; i < a.length; i++){ if (a[i].textContent != '') { button.disabled(false); break; } } //Si le bouton est toujours inactif //(c'est donc qu'on n'a pas encore trouvé de texte) //et qu'un bloc suit le container de l'image, //on n'active le bouton que si ce bloc contient du texte et est dépourvu d'image. if (button.disabled() && dom.getNext(imgContainer,dom.isBlock)) { var myNext = dom.getNext(imgContainer,dom.isBlock); if (myNext.textContent != '' && myNext.getElementsByTagName('img').length == 0) button.disabled(false); } } } } } }); /******************************************* AJOUT DE NOTRE BOUTON DANS LA BARRE D'OUTILS ******************************************** http://archive.tinymce.com/wiki.php/api4:method.tinymce.Editor.addButton */ editor.addButton('textFloatingImageTinyMCE', { title: 'Aligner verticalement texte & image', //L'image du bouton nommée "icon.png" est rangée dans : //repertoire_Wordpress/wp-content/plugins/text-floating-image/admin/img //url vaut : //repertoire_Wordpress/wp-content/plugins/text-floating-image/admin/js //On retire les deux derniers caractères de url et on y ajoute 'img/icon.png' //pour obtenir l'adresse de l'image : //repertoire_Wordpress/wp-content/plugins/text-floating-image/admin/img/icon.png image: url.substr(0, url.length-2) + 'img/icon.png', onPostRender: function() { //On récupère une référence à notre bouton button = this; //On part à la recherche des boutons d'alignement //pour pouvoir par la suite forcer leur état à disabled quand //l'utilisateur sélectionne une image d'ores et déjà traitée. //http://community.tinymce.com/forum/viewtopic.php?id=31104 var buttons = editor.theme.panel.find('toolbar *'); for (var i = 0; i < buttons.length; i++) { switch (buttons[i]._aria.label) { case 'Align left': alignLeftButton = buttons[i]; break; case 'Align center': alignCenterButton = buttons[i]; break; case 'Align right': alignRightButton = buttons[i]; break; } } }, onclick: function() { if (imgContainer.textContent != '') { //Si l'image est incluse dans un bloc contenant du texte, //on demande poliment l'autorisation de l'isoler avant de continuer //Néanmoins, l'isolation ne sera réalisée que si l'utilisateur valide //la fenêtre de paramétrage (fonction onSubmit de la fenêtre). //https://www.tinymce.com/docs/api/tinymce/tinymce.windowmanager/ editor.windowManager.confirm("L'image est incluse dans un bloc\nqui contient du texte.\nAcceptez-vous que je l'isole pour continuer?", function(reponse) { if (reponse) { //On lance la procédure... main(); } } ); } else { //On lance la procédure... main(); } }, }); function main() { alert("Bientôt s'affichera sur votre écran une splendide fenêtre de paramétrage !!!"); } }); })(); |
Je vous laisse étudier, tester et re-tester l’ensemble jusqu’à plus soif…
Récupération des options enregistrées
Si nous nous sommes échinés à proposer des options modifiables à nos utilisateurs, ce n’est pas pour les laisser pourrir dans leur coin. Et s’il y a bien un endroit où elles vont pouvoir se rendre utiles, c’est dans la fenêtre de paramétrage que nous n’allons pas tarder à construire.
Le petit problème, c’est que ces options sont proprement rangées dans la base de données de maître WordPress et que pour pouvoir y accéder, il nous faut causer php. Or, pas de chance, nous œuvrons actuellement dans un fichier javascript…
Il nous faut donc vaillamment construire de nos petites mains agiles un pont entre ces deux mondes.
La technique généralement utilisée dans ce genre de problématique consiste à s’appuyer sur le hook admin_head-post.php (en bref, une extension du hook admin_head qui ne se déclenche que lorsque l’on édite une page ou un article) pour écrire un bout de javascript dans l’en-tête de la page d’édition. Le petit souci de ce hook, c’est qu’il demeure désespérément au rang des grands absents si on crée un nouveau post. Dans ce cas précis, c’est sur son petit cousin admin_head-post-new.php qu’il faut se reposer.
Quoi qu’il en soit, la pratique devrait vous éclairer bien mieux que de plus longs et verbeux discours. Dans la classe admin/class-text-floating-image-admin.php, nous créons la fonction pass_options_to_tinyMCE() que voici :
257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 |
/** * Enregistrement des options de l'extension dans un objet javascript * La fonction est appelée sur les hook admin_head-post-new.php et admin_head-post.php * dans la fonction define_admin_hooks() de includes/class-text-floating-image.php * * @since 1.0.0 */ function pass_options_to_tinyMCE() { $options = get_option($this->plugin_name); ?> <script type='text/javascript'> var text_floating_image_options = <?php echo json_encode($options); ?>; </script> <?php } |
Et nous revenons une fois de plus au sein de la fonction define_admin_hooks() de notre classe dirigeante includes/class-text-floating-image.php :
216 217 218 219 220 221 222 223 224 225 226 |
/** * Enregistrement des options de l'extension dans un objet javascript * chargé dans la page de création ('admin_head-post-new.php') * ou d'édition d'un post ('admin_head-post.php'). * La fonction pass_options_to_tinyMCE est appelée dans la classe * admin/class-text-floating-image-admin.php. */ $this->loader->add_action('admin_head-post-new.php', $plugin_admin, 'pass_options_to_tinyMCE'); $this->loader->add_action('admin_head-post.php', $plugin_admin, 'pass_options_to_tinyMCE'); |
Si vous ouvrez votre backend de manière à créer ou modifier une page ou un article et que vous fouillez un peu les entrailles de la bête dans l’inspecteur de votre navigateur favori, vous devriez désormais voir figurer notre nouveau script dans l’en-tête. Vous constaterez par ailleurs en naviguant dans les diverses autres sections du tableau de bord que ce même script en est absent. À croire que nous sommes effectivement parvenus à nos fins !
Création de la fenêtre de paramétrage
Plus rien ne semble plus nous retenir de nous attaquer à la construction de notre fenêtre de paramétrage.
Nous l’avons vu, celle-ci sera affichée suite au clic sur notre bouton judicieusement inséré dans la barre d’outils de tinyMCE. Cependant, si l’image est incluse dans un bloc comprenant du texte, on demande au préalable l’autorisation de procéder à son isolation dans un bloc indépendant. Si l’utilisateur nous oppose son refus, on arrête là tout en restant bons amis. S’il accepte notre proposition même pas malhonnête, on lance la procédure soigneusement empaquetée dans une fonction main() qui comme son nom l’indique se charge du gros du boulot. Si d’aventure l’image réside d’ores et déjà en solitaire dans un bloc à elle seule dédiée, on passe à la fonction main() sans autre forme de procès.
Notre fenêtre comportera les options suivantes :
- Un champ pour la largeur de l’image en pourcentage.
- Une case à cocher pour spécifier son alignement.
- Un champ pour sa marge à gauche.
- Un autre pour sa marge à droite.
- Un champ supplémentaire que la pratique m’a incité à ajouter afin de pouvoir spécifier l’espace à ménager après l’ensemble image/texte : le fait est que si nous faisons se succéder deux blocs d’alignement et que, pour chacun d’eux, le texte entoure l’image, le résultat apparaît comme un seul gros bloc continu d’aspect potentiellement indigeste. Ce nouveau paramètre permet de redonner un peu d’air à l’ensemble en insérant un espace entre les deux blocs. Concrètement, la valeur spécifiée dans ce champs définira la hauteur de l’élément stylé en clear:both que nous introduirons à la suite du dernier bloc de texte pour clore notre effet.
- Un bouton Reset pour rendre aux champs définis ci-dessus leur valeur par défaut.
- Un bouton – permettant le cas échéant de réduire d’un bloc la sélection de texte.
- Un bouton + qui assure au contraire et quand c’est possible l’augmentation de la sélection de texte d’un bloc.
- Un bouton OK pour valider le tout.
- Un bouton Annuler pour… euh… pour quoi faire ?, hein !, à votre avis ?
La largeur, l’alignement et les marges définies par notre utilisateur seront stockées comme prévu dans un attribut data-text_floating_image-style porté par l’image. Pour des raisons de commodités de codage, la valeur de l’espace à ménager après notre ensemble sera elle enregistrée dans un attribut dédié, data-text_floating_image-after, lui aussi porté par l’image.
Que voici donc le code abondamment commenté issu de mes cogitations :
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 |
(function() { 'use strict'; //Enregistrement du plugin auprès de tinymce tinymce.PluginManager.add('textFloatingImageTinyMCE', function(editor, url) { /* * editor est une référence à l'éditeur de tinyMCE avec lequel on interagit * url est le lien de notre fichier */ //DÉCLARATION DES VARIABLES var dom,//référence au dom de l'éditeur. Initialisée dans l'événement init de l'éditeur selection,//L'objet selection de l'éditeur. Initialisée dans l'événement init de l'éditeur img,//L'image sélectionnée. Initialisée dans l'événement NodeChange //dès lors que l'élément sélectionné est une image imgContainer,//Le bloc contenant l'image. Initialisée dans l'événement NodeChange //dès lors que l'élément sélectionné est une image button,//Le bouton de notre extension ajouté à la barre d'outils de tinyMCE alignLeftButton, alignCenterButton, alignRightButton, //Une référence aux trois boutons d'alignement pour pouvoir gérer leur état //Toutes les variables boutons sont initialisées dans la fonction addButton prevEl,//une variable pour gérer plus judicieusement l'événement NodeChange //qui se déclenche deux fois id,//L'id qui sera affecté à l'attribut data-text_floating_image. //Initialisé dans la fonction main myWindow,//Notre popup de paramétrage. Initialisée dans la fonction main $ = tinymce.dom.DomQuery;//http://archive.tinymce.com/wiki.php/api4:class.tinymce.dom.DomQuery editor.on('init', function(e) { dom = editor.dom; selection = editor.selection; }); /***************************************************************************** GESTION DE L'ÉTAT DES BOUTONS DE LA BARRE D'OUTILS EN FONCTION DE LA SÉLECTION ****************************************************************************** On s'appuie sur l'évènement "NodeChange" de l'éditeur. http://archive.tinymce.com/wiki.php/api4:event.tinymce.Editor.NodeChange */ editor.on('NodeChange', function() { //On stocke l'élément sélectionné dans l'éditeur //http://archive.tinymce.com/wiki.php/api4:method.tinymce.dom.Selection.getNode var el = selection.getNode(); //L'événement "NodeChange" a tendance a se déclencher deux fois. //Pour alléger la charge de travail, on n'exécute notre tâche que si l'on constate //par nous-même que l'élément actuellement sélectionné est bien différent //de l'élément précédemment sélectionné, lequel est stocké dans la variable prevEL if(el != prevEl) { prevEl = el; //Reset de l'état des boutons button.disabled(true); button.active(false); alignLeftButton.disabled(false); alignCenterButton.disabled(false); alignRightButton.disabled(false); //Si l'élément sélectionné est une image if (el.nodeName == 'IMG') { img = el; //Initialisation de la variable imgContainer imgContainer = dom.getParent(img,dom.isBlock); //Si l'extension est d'ores et déjà appliquée à l'image if (img.hasAttribute('data-text_floating_image')) { button.disabled(false); button.active(true); //On désactive les boutons d'alignement alignLeftButton.disabled(true); alignCenterButton.disabled(true); alignRightButton.disabled(true); //Sinon et si l'image n'est pas incluse dans un élément li } else if (imgContainer.nodeName != 'LI') { //On s'assure avant tout, qu'au sein même de son block container, //l'image n'est pas suivie d'une autre image. //Pour ce faire, on convertit en tableau la collection HTML //des images contenues dans le container //et on vérifie que l'image sélectionnée est bien le dernier élément //de ce tableau //http://stackoverflow.com/a/222847 var arr = [].slice.call(imgContainer.getElementsByTagName('img')); if (arr.indexOf(img) == arr.length - 1) { //Si l'image est bien la dernière de son block container, //on recherche la présence de texte après l'image //au sein de son container //et, dès lors qu'on en trouve, on active le bouton //http://stackoverflow.com/a/10730777 var n, a = [], walk = editor.getDoc().createTreeWalker(imgContainer, NodeFilter.SHOW_ALL, null, false); //On construit un tableau des noeuds contenus dans le container while(n = walk.nextNode()) a.push(n); //On réduit ce tableau aux noeuds qui suivent l'image a.splice(0, a.indexOf(img) + 1); //Dès qu'on détecte un noeud contenant du texte, //on active le bouton et on passe à la suite for(var i = 0; i < a.length; i++){ if (a[i].textContent != '') { button.disabled(false); break; } } //Si le bouton est toujours inactif //(c'est donc qu'on n'a pas encore trouvé de texte) //et qu'un bloc suit le container de l'image, //on n'active le bouton que si ce bloc contient du texte et est dépourvu d'image. if (button.disabled() && dom.getNext(imgContainer,dom.isBlock)) { var myNext = dom.getNext(imgContainer,dom.isBlock); if (myNext.textContent != '' && myNext.getElementsByTagName('img').length == 0) button.disabled(false); } } } } } }); /******************************************* AJOUT DE NOTRE BOUTON DANS LA BARRE D'OUTILS ******************************************** http://archive.tinymce.com/wiki.php/api4:method.tinymce.Editor.addButton */ editor.addButton('textFloatingImageTinyMCE', { title: 'Aligner verticalement texte & image', //L'image du bouton nommée "icon.png" est rangée dans : //repertoire_Wordpress/wp-content/plugins/text-floating-image/admin/img //url vaut : //repertoire_Wordpress/wp-content/plugins/text-floating-image/admin/js //On retire les deux derniers caractères de url et on y ajoute 'img/icon.png' //pour obtenir l'adresse de l'image : //repertoire_Wordpress/wp-content/plugins/text-floating-image/admin/img/icon.png image: url.substr(0, url.length-2) + 'img/icon.png', onPostRender: function() { //On récupère une référence à notre bouton button = this; //On part à la recherche des boutons d'alignement //pour pouvoir par la suite forcer leur état à disabled quand //l'utilisateur sélectionne une image d'ores et déjà traitée. //http://community.tinymce.com/forum/viewtopic.php?id=31104 var buttons = editor.theme.panel.find('toolbar *'); for (var i = 0; i < buttons.length; i++) { switch (buttons[i]._aria.label) { case 'Align left': alignLeftButton = buttons[i]; break; case 'Align center': alignCenterButton = buttons[i]; break; case 'Align right': alignRightButton = buttons[i]; break; } } }, onclick: function() { if (imgContainer.textContent != '') { //Si l'image est incluse dans un bloc contenant du texte, //on demande poliment l'autorisation de l'isoler avant de continuer //Néanmoins, l'isolation ne sera réalisée que si l'utilisateur valide //la fenêtre de paramétrage (fonction onSubmit de la fenêtre). //https://www.tinymce.com/docs/api/tinymce/tinymce.windowmanager/ editor.windowManager.confirm("L'image est incluse dans un bloc\nqui contient du texte.\nAcceptez-vous que je l'isole pour continuer?", function(reponse) { if (reponse) { //On lance la procédure... main(); } } ); } else { //On lance la procédure... main(); } }, }); function main() { var boutonPlus,//Référence au bouton + de la fenêtre boutonMoins,//Référence au bouton - de la fenêtre //Récupération des options imageWidth = text_floating_image_options.imageWidth, alignRight = Boolean(text_floating_image_options.alignRight), left_marginLeft = text_floating_image_options.left_marginLeft, left_marginRight = text_floating_image_options.left_marginRight, right_marginLeft = text_floating_image_options.right_marginLeft, right_marginRight = text_floating_image_options.right_marginRight, spaceAfter = 0, //On considére par défaut qu'il ne s'agit pas d'un update update = false; //Si l'image dispose déjà d'un attribut data-text_floating_image //et qu'il s'agit donc d'un update if (img.getAttribute('data-text_floating_image')) { //Il s'agit d'un update update = true; //On conserve son id id = img.getAttribute('data-text_floating_image'); //On transforme le contenu de son attribut data-text_floating_image-style //en objet pour en faciliter la lecture var prop = parseAttr(img.getAttribute('data-text_floating_image-style')); //On récupère les paramètres enregistrés pour les afficher imageWidth = parseInt(prop.width); alignRight = (prop.clear == 'left')?0:1; left_marginLeft = (prop.clear == 'left')?parseInt(prop['margin-left']):left_marginLeft; left_marginRight = (prop.clear == 'left')?parseInt(prop['margin-right']):left_marginRight; right_marginLeft = (prop.clear == 'left')?right_marginLeft:parseInt(prop['margin-left']); right_marginRight = (prop.clear == 'left')?right_marginRight:parseInt(prop['margin-right']); spaceAfter = img.getAttribute('data-text_floating_image-after'); //TODO : sélectionner le texte lié } else { //Calcul de l'id que nous attribuerons à l'image id = new Date().valueOf(); //TODO : sélectionner le premier bloc de texte disponible pour l'alignement } //CREATION DES BOUTONS OK ET ANNULER //AINSI QUE, LE CAS ÉCHÉANT, DU BOUTON SUPPRIMER var buttons = [ { text: "OK", subtype: 'primary', onclick: function(){ //On enregistre les paramètres myWindow.find('form')[0].submit(); } }, { text: "Annuler", onclick: function(){ //On referme la fenêtre myWindow.close(); } } ]; //S'il s'agit d'un update, on intercale entre les deux boutons précédemment créés //un troisième bouton "Supprimer" pour supprimer le plugin appliqué sur l'image if (update) { buttons.splice(1,0, { text: "Supprimer", classes: 'text_floating_image-suppress', subtype: 'suppress', onclick: suppress } ); } //FIN DE LA CRÉATION DES BOUTONS //On enregistre une référence à notre fenêtre dans la variable myWindow myWindow = editor.windowManager.open({ //CONTENU DE LA FENÊTRE title: "Paramètres d'alignement", body: [ { classes: 'text_floating_image-input', type: 'textbox', name: 'width', label: "Largeur de l'image en pourcentage :", value: imageWidth, min:1, max:100, }, { classes: 'text_floating_image-checkbox', type: 'checkbox', name: 'align', label: "Alignement à droite :", tooltip: "Cocher pour aligner l'image à droite", checked: alignRight, onchange: function(){ alignRight = !alignRight; //Attribution des valeurs enregistrées pour les marges //en fonction de l'alignement if (this.checked()) { myWindow.find('#marginLeft').value(right_marginLeft); myWindow.find('#marginRight').value(right_marginRight); } else { myWindow.find('#marginLeft').value(left_marginLeft); myWindow.find('#marginRight').value(left_marginRight); } } }, { classes: 'text_floating_image-input', type: 'textbox', name: 'marginLeft', label: "Marge gauche :", value: (alignRight)?right_marginLeft:left_marginLeft, min: 0 }, { classes: 'text_floating_image-input', type: 'textbox', name: 'marginRight', label: "Marge droite :", value: (alignRight)?right_marginRight:left_marginRight, min: 0 }, { classes: 'text_floating_image-input', type: 'textbox', name: 'spaceAfter', label: "Espace après :", value: spaceAfter, min: 0 }, { type:'container', classes: 'text_floating_image-resetContainer', items: [ { type: 'button', text: "Reset", size:'medium', tooltip: 'Réinitialiser les paramètres ci-dessus', onclick:function(){ //Rétablissement des paramètres par défaut myWindow.find('#width').value(text_floating_image_options.imageWidth); myWindow.find('#align').checked(Boolean(text_floating_image_options.alignRight)); myWindow.find('#spaceAfter').value(0); //Application de la marge gauche par défaut en fonction de l'alignement var m = Boolean(text_floating_image_options.alignRight)? text_floating_image_options.right_marginLeft: text_floating_image_options.left_marginLeft; myWindow.find('#marginLeft').value(m); //Application de la marge droite par défaut en fonction de l'alignement m = Boolean(text_floating_image_options.alignRight)? text_floating_image_options.right_marginRight: text_floating_image_options.left_marginRight; myWindow.find('#marginRight').value(m); //On passe le focus au premier champs $('.mce-text_floating_image-input')[0].select(); } }, ], }, { type: 'fieldset', classes: 'text_floating_image-fieldset', items: [ { type: 'label', text: 'Blocs de texte à aligner :', }, { type: 'buttongroup', classes: 'text_floating_image-buttonGroup', border: '0 0 0 0', items: [ { text:'-', tooltip: "Diminuer le texte aligné d'un bloc", onPostRender:function(){ boutonMoins = this; //TODO : désactiver le bouton si un seul bloc de //texte est sélectionné pour l'alignement }, onClick:function(){ //TODO : gérer l'état en fonction du nombre de //blocs de texte sélectionnés pour l'alignement }, }, { text: '+', tooltip: "Augmenter le texte aligné d'un bloc", onPostRender:function(){ boutonPlus = this; //TODO : désactiver le bouton si tous les blocs de //texte disponibles pour l'alignement //sont d'ores et déjà sélectionnés }, onClick:function(){ //TODO : gérer l'état en fonction du nombre de //blocs de texte disponibles pour l'alignement }, } ], }, ], }, ], buttons: buttons, //FIN CONTENU DE LA FENÊTRE onSubmit: function(e) { //Si la largeur rentrée n'est pas acceptable if (e.data.width < 1 || e.data.width > 99) { //On bloque et on passe le focus au premier champs e.preventDefault(); $('.mce-text_floating_image-input')[0].select(); } else { //S'il s'agit d'un update if (update) { //TODO : supprimer les attributs data-text_floating_image //des blocs de texte assignés à l'alignement //Sinon } else { //On passe l'id à l'attribut data-text_floating_image de l'image dom.setAttrib(img, 'data-text_floating_image', id); //Si le container de l'image contient du texte, //nous devons procéder à l'isolation de l'image if (imgContainer.textContent != '') { //Si l'image est contenue dans une ancre //monElement = ancre //sinon //monElement = img var monElement = (img.parentElement.nodeName == "A")?img.parentElement:img; //on effectue un split //http://archive.tinymce.com/wiki.php/api4:method.tinymce.dom.DOMUtils.split dom.split(imgContainer,monElement); //et on wrappe monElement dans un paragraphe //http://archive.tinymce.com/wiki.php/api4:method.tinymce.dom.DomQuery.wrap $(monElement).wrap('p'); //On met à jour la variable imgContainer imgContainer = monElement.parentElement; } } //TODO : créer l'attribut data-text_floating_image portant la valeur id //sur chacun des blocs de texte sélectionnés //On donne une forme textuelle qui va bien au paramètre d'alignement var myAlign = (e.data.align)?'right':'left'; //On forme une chaine à partir des paramètres entrés par le rédacteur var str = 'width:' + e.data.width + '%' + ';clear:' + myAlign + ';float:' + myAlign + ';margin-left:' + e.data.marginLeft + 'px' + ';margin-right:' + e.data.marginRight + 'px;'; //Et on l'affecte à l'attribut data-text_floating_image-style de l'image. dom.setAttrib(img, 'data-text_floating_image-style', str); dom.setAttrib(img, 'data-text_floating_image-after', e.data.spaceAfter); if (update) { //Gestion de l'update } else { //On sauvegarde les classes de l'image dom.setAttrib(img, 'data-text_floating_image-classes', img.className); //avant de les supprimer img.removeAttribute('class'); //On empêche l'apparition de la barre d'outils du plugin image //Voir la function isPlaceholder() au début de //wp-includes/js/tinymce/plugins/wpeditimage/plugin.js dom.setAttrib(img, 'data-mce-placeholder', 1); //TODO : mise à jour de l'éditeur } } } }); //On donne l'attibut number aux champs de notre fenêtre $('.mce-text_floating_image-input').attr('type','number'); //On sélectionne le contenu du premier champs //Sans setTimeout, la sélection risque d'être inopérante. setTimeout(function(){$('.mce-text_floating_image-input')[0].select();},1) } //Fonction appelée par le clic sur le bouton "Supprimer" //du popup de paramétrage function suppress() { //TODO : tout myWindow.close(); } /********** UTILITAIRES *********** Conversion d'une chaine de la forme "width:60%;clear:right;float:right;margin-left:20px;margin-right:0px;" en objet : { width:"60%", clear:"right", float:"right", margin-left:"20px", margin-right:"0px" } http://stackoverflow.com/a/33030078 */ function parseAttr(string) { var regex = /([\w-]*)\s*:\s*([^;]*)/g; var match, prop={}; while(match=regex.exec(string)) prop[match[1]] = match[2]; return prop; } }); })(); |
Le tableau serait incomplet sans les quelques lignes que nous rajoutons à notre feuille de style :
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 |
/**************************** POPUP DE PARAMÉTRAGE *****************************/ .mce-text_floating_image-input { text-align: right !important; width: 3em !important; height:1.5em !important; padding:0 !important; font-weight: bold !important; } .mce-text_floating_image-checkbox{ text-align: right !important; } .mce-text_floating_image-suppress{ background: #990000 none repeat scroll 0 0 !important; border-color: #000000 !important; box-shadow: 0 1px 0 #bbbbbb !important; } .mce-text_floating_image-suppress:hover{ background: #880000 none repeat scroll 0 0 !important; } .mce-text_floating_image-suppress > button{ color: #fff !important; } |
Bon, j’avoue : voilà qui fait encore un gros pavé à digérer !
En même temps, au vu des mentions TODO, vous vous doutez que nous ne sommes pas au bout de nos peines.
Nous allons néanmoins nous arrêter là pour aujourd’hui, respirer un bon bol d’air, nous aérer la tête et le reste avant de songer à la suite et de nous rapprocher petits pas à petits pas du but que nous nous sommes fixés…