Développer un plugin pour WordPress
5 – Moteur du plugin tinyMCE
18 octobre 2016.
Bilan provisoire
Nous avons jusqu’ici développé une mécanique maison qui réalise déjà quelques petites choses pas inintéressantes :
- Une page dédiée au réglage des valeurs par défaut, accessible tout à la fois depuis la page des extensions et le menu Réglages de WordPress.
- L’ajout dans la barre d’outils de l’éditeur d’un bouton qui ne s’active que lorsque notre extension est applicable.
- Une fenêtre de paramétrage permettant au rédacteur d’ajuster tout à loisir les caractéristiques de l’image mais aussi d’élargir ou de réduire la portion de texte impliquée dans l’alignement.
Les esprits chagrins ne manqueront cependant pas de souligner qu’il nous reste encore beaucoup à faire :
- En premier lieu, nous débrouiller pour que notre fenêtre de paramétrage n’affecte pas comme elle le fait actuellement la visibilité sur le contenu de l’article. Il s’agira de la positionner dans le coin supérieur droit, de diminuer l’opacité du cadre modal qui l’entoure et d’autoriser par la ruse le scroll sur l’article de façon à délivrer un meilleur aperçu du texte sélectionné.
- À l’ouverture du pop-up :
- s’il s’agit d’une première application de notre extension, assurer la sélection du premier bloc de texte qui suit l’image.
- s’il s’agit d’un update, sélectionner tous les blocs de texte impliqués dans l’alignement.
- Gérer au mieux l’état des boutons + et – en fonction du texte susceptible d’être impliqué dans l’alignement.
- Mettre enfin en place la mécanique qui permettra d’assurer une visualisation de l’alignement dans l’éditeur tout en gérant les éventuelles modifications des paramètres y compris la suppression de l’effet.
- Assurer une transition sans encombre du mode Visuel au mode Texte de l’éditeur.
- Surveiller les modifications apportées par le rédacteur au contenu affecté par notre extension (déplacement ou suppression de l’image, modification du texte, etc.) et rectifier la structure en conséquence.
Donner de la visibilité au contenu
Voilà qui sonne comme le slogan d’un professionnel du référencement. Mais il s’agit en l’occurrence de tout autre chose : en l’état, notre pop-up de paramétrage apparaît au centre de la fenêtre du navigateur sur un fond sombre qui dissimule en grande partie le corps de l’article. L’aspect modal n’est certes pas dépourvu d’intérêt en ce sens qu’il limite les interventions de l’utilisateur à notre fenêtre et à elle seule et prévient donc toute modification intempestive du contenu durant l’édition des paramètres. Néanmoins, il restreint drastiquement la vision d’ensemble d’autant plus qu’il interdit de scroller au sein de l’article pour garder un œil sur les portions de texte sélectionnées.
Pour régler cet épineux problème, nous nous appuyons sur l’événement onOpen de notre fenêtre ainsi que sur quelques petits bouts de jQuery auquel nous sommes contraints de recourir pour palier les insuffisances de la classe DomQuery de tinyMCE qui n’implémente pas la gestion des dimensions.
Que voici donc un aperçu de la chose :
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 |
//On enregistre une référence à notre fenêtre dans la variable myWindow myWindow = editor.windowManager.open({ onOpen: function(){ //Un petit tour dans l'inspecteur nous apprend que myWindow //dispose d'une propriété $el qui renvoie un tableau //dont le premier élément fait justement référence à notre fenêtre //Ne reste plus alors qu'à positionner cette dernière en haut à droite myWindow.$el[0].style.top = 0; myWindow.$el[0].style.left = 'auto'; myWindow.$el[0].style.right = 0; //On réduit l'opacité de la fenêtre modale //pour assurer une visualisation correcte de la sélection de texte //L'inspecteur nous apprend qu'elle porte un id 'mce-modal-block' $('#mce-modal-block').css('opacity',0); //La fenêtre modale bloque le scrolling sur l'éditeur //et empêche le cas échéant de visualiser le texte sélectionné //On pose donc un écouteur sur l'événement wheel //qui permet de rétablir le scroll de l'éditeur //Référence à l'iframe contenant l'éditeur var $iframe = jQuery('#content_ifr'); //Référence au contenu de l'iframe pour en obtenir la hauteur var $iframeContents = $iframe.contents(); jQuery(window).on('wheel', function(e){ //Valeur du delta appliqué au scroll var delta = 40; //Affectation du signe du delta en fonction du mouvement de la molette : //S'il s'agit d'une descente, delta est positif et on augmente la valeur de scrollTop //tant qu'on n'a pas atteint son maximum possible //Sinon, delta est négatif et on diminue la valeur de scrollTop //tant qu'il est supérieur à zéro delta = (e.originalEvent.deltaY > 0)?delta:-delta; //On stocke la valeur actuelle du scrollTop var scroll = $iframeContents.scrollTop(); //Si delta est négatif et que l'iframe n'est pas remontée au max //Ou si delta est positif et que l'iframe n'est pas descendue au max if ((delta < 0 && scroll + delta > 0) || (delta > 0 && $iframe.height() + scroll < $iframeContents.height())) { //On bloque l'événement sur l'élément window e.preventDefault(); //Et on applique le scroll sur l'iframe $iframeContents.scrollTop(scroll + delta); } }) }, //CONTENU DE LA FENÊTRE |
Les mains dans le cambouis
Pour la gestion des blocs de texte, nous allons recourir à deux tableaux : selectedTextArray qui contiendra les blocs de texte impliqués dans l’alignement et availableTextArray qui recensera tous les blocs de textes susceptibles de participer à l’alignement. Ceux qui gardent envers et contre tout quelques souvenirs de la théorie des ensembles étudiée dans leurs jeunes années conviendront sans peine que le premier est inclus dans le second. La comparaison entre ces deux tableaux nous permettra par ailleurs de gérer l’état des boutons + et – de notre pop-up.
Pour opérer toute la petite cuisine autour de l’image, nous nous appuierons sur un objet Tfi construit sur mesure à partir de l’image qu’il reçoit en paramètre.
Cet objet va disposer des propriétés suivantes :
- id : identifiant unique stocké dans l’attribut data-text_floating_image porté par l’image (ainsi que par les blocs de texte qui lui sont apparentés).
- texts : tableau des blocs de texte lié à l’image.
- first : référence au premier de ces blocs de texte.
- last : référence au dernier de ces blocs de texte.
Et des méthodes que voici-voilà :
- suppress : assure le nettoyage en cas de suppression de l’effet.
- clean : lors de la sauvegarde de l’article, débarrasse le code des différents éléments et autres ajustements stylistiques introduits pour assurer la prévisualisation et assure ainsi l’enregistrement d’une structure html dénuée de toute scorie inutile.
- preUpdate : effectue un reset de la structure avant de procéder à l’update d’une image déjà traitée dont on modifie les paramètres.
- update : construit la structure nécessaire en introduisant les div preElastik et elastik avant le bloc container de l’image et en rajoutant au dernier des blocs de texte une classe dont on reparle dans quelques lignes.
- resize : réalise la mise en place effective des éléments les uns par rapport aux autres en fonction de leurs dimensions respectives.
À chaque fois qu’une image se voit appliquer notre extension, nous générons une instance de l’objet Tfi et l’ajoutons à un tableau tfiArray destiné à recenser toutes les instances ainsi créées. À chaque image traitée correspond donc une instance tfi ayant comme propriété id la même valeur que l’attribut data-text_floating_image de l’image en question. Ainsi n’est-il donc pas franchement sorcier, en parcourant le tableau tfiArray, de retrouver l’instance tfi correspondant à une image à partir de la valeur enregistrée dans l’attribut data-text_floating_image de celle-ci.
Plutôt que d’introduire un élément stylé avec clear:both après le dernier bloc de texte pour mettre fin à l’effet flottant et courir subséquemment le risque conséquent que celui-ci soit inopinément supprimé ensuite par le rédacteur, il m’a semblé plus judicieux de recourir à un pseudo élément ::after apposé sur ce dernier bloc lui-même. La hauteur de ce pseudo élément correspondra à la valeur entrée pour le champ Espace après de notre pop-up. Le problème majeur de cette élégante option est que le langage javascript ne nous permet pas d’accéder aux pseudo-éléments. Pour contourner l’obstacle, nous développerons une solution similaire à celle exposée ici : on inscrit dans l’en-tête de l’éditeur de tinyMCE un élément <style> déclarant une classe répondant à nos besoins. Il ne nous reste plus alors qu’à attribuer cette classe au bloc que nous souhaitons traiter.
Changement du mode d’édition
Le passage du mode Visuel au mode Texte est détecté par un écouteur posé sur l’événement click du bouton Texte de l’éditeur. Cet écouteur déclenche la fonction clean des instances de l’objet Tfi, fonction qui est justement chargée d’éliminer toutes les traces de nos petites bidouilles destinées à la prévisualisation de l’effet.
Le retour au mode visuel est détecté par un écouteur posé sur l’événement show de l’éditeur. Il nous suffit alors de recréer une instance de Tfi pour chaque image portant l’attribut data-text_floating_image.
La NSA n’a pas le monopole de la surveillance
Une fois notre extension appliquée à une image, nous devons nous assurer que le rédacteur ne va pas bousiller notre grand oeuvre en éditant comme un cochon. En même temps, on ne peut pas l’en empêcher… Mais, on peut en retour prendre quelques mesures, au besoin radicales…
Ainsi donc, j’ai décidé en tant que moi-même qu’un déplacement de l’image provoquerait la suppression immédiate de l’effet. De même si l’on constate l’insertion d’une image au sein d’un des blocs impliqués dans l’alignement. Itou s’il s’avère qu’il ne reste qu’un bloc aligné et que ce bloc est dépourvu de texte. Et toc !
Bien évidemment, nous devons tout autant nettoyer la structure dès lors que notre utilisateur choisit de supprimer l’image seule ou un ensemble comportant l’image.
Enfin, il nous faut prendre en compte le cas où notre utilisateur choisit de rédiger un nouveau paragraphe au sein même des blocs de texte participant à l’alignement.
Pour mettre en place cette surveillance, nous nous reposerons sur la nouvelle API javascript MutationObserver dont nous exploiterons sans vergogne la puissance.
Cependant, il demeure un paramètre sur lequel notre utilisateur est encore libre d’intervenir pour foutre la pagaille : ce fourbe peut tenter de redimensionner l’image. Sauf que tinyMCE a la judicieuse idée d’offrir dans sa phase d’initialisation une option permettant de spécifier les éléments disposant des poignées de redimensionnement. Nous n’allons bien évidemment pas nous priver du malin plaisir de bloquer cette possibilité sur les images portant l’attribut data-text_floating_image. Et ce pas plus tard que maintenant, d’autant que la phase d’initialisation de tinyMCE est aussi celle où l’on peut définir certains éléments de style que l’on veut voir s’appliquer au sein de l’éditeur. Nous en profiterons donc pour établir une largeur de 100% sur nos images afin de nous assurer que, quoiqu’il arrive, elles occupent toujours toute la largeur mise à leur disposition par leur paragraphe container (que les inquiets se rassurent : le style height:auto nécessaire par ailleurs pour éviter toute déformation de l’image est prévu par WordPress dans sa feuille wp-content.css).
Un petit saut dans la classe admin/class-text-floating-image-admin.php :
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 |
/** * Définition des styles propres à notre extension au sein de l'éditeur * et blocage du redimensionnement sur les images portant l'attribut data-text_floating_image * La fonction est appelée sur le hook tiny_mce_before_init * dans la fonction define_admin_hooks() de includes/class-text-floating-image.php * //https://www.mattcromwell.com/dynamic-tinymce-editor-styles-wordpress/ * * @since 1.0.0 */ function add_editor_style($mceInit) { //On impose une largeur de 100% aux images portant l'attribut //data-text_floating_image. //Pour rappel, cet attribut est spécifique des images traitées //par notre extension. $styles = "img[data-text_floating_image]{width: 100% !important;}"; if (!isset( $mceInit['content_style'])) { $mceInit['content_style'] = $styles.' '; } else { $mceInit['content_style'] .= ' '.$styles .' '; } //On autorise le redeimensionnement de tous les éléments //à l'exception des images traitées par notre extension $mceInit['object_resizing'] = "*:not(img[data-text_floating_image])"; return $mceInit; } |
Et un autre dans includes/class-text-floating-image.php :
223 224 225 226 227 228 229 230 231 |
/** * Définition des styles propres à notre extension au sein de l'éditeur * et blocage du redimensionnement sur les images portant l'attribut data-text_floating_image * La fonction add_editor_style réside dans la classe * admin/class-text-floating-image-admin.php. */ $this->loader->add_filter('tiny_mce_before_init', $plugin_admin, 'add_editor_style'); |
Vite fait, bien fait !
Un gros pavé
Ne me reste donc plus (pour l’instant !) qu’à vous délivrer le contenu total révisé commenté vérifié contrôlé mais faut pas rêver comportant certainement son lot de bugs encore à déceler :
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 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 |
(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 body,//référence au body de l'éditeur. Initialisée dans l'événement init de l'éditeur doc,//référence au document 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 selectedTextArray = [],//reseté dans NodeChange. Construit dans la fontion main observer,//Le MutationObserver posé pour surveiller les modifications dans l'éditeur tfiArray = [];//Tableau des instances tfi editor.on('init', function(e) { dom = editor.dom; body = editor.getBody(); doc = editor.getDoc(); selection = editor.selection; //On crée une instance tfi pour chaque image portant l'attribut //data-text_floating_image dom.select('img[data-text_floating_image]').forEach(function(el){ new Tfi(el); }); //Construction d'observer qui va réaliser un update de l'objet tfi //lors de l'ajout d'un paragraphe dans un bloc de paragraphes liés //et la suppression du plugin : // - lors de l'ajout d'une image dans un paragraphe lié // - lors du déplacement d'une image traitée // - lors de la suppresion d'une image traitée ou d'un ensemble comprenant une image traitée // - s'il ne reste qu'un paragraphe lié ne contenant plus de texte observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { //Ajout d'éléments for (var i = 0; i < mutation.addedNodes.length; i++) { if (mutation.addedNodes[i] instanceof HTMLElement) { var nodeAdded = mutation.addedNodes[i]; if (nodeAdded.hasAttribute('data-text_floating_image')) { //Ajout d'un paragraphe lié //Si le rédacteur ajoute un paragraphe en faisant 'Entrée' //ce paragraphe porte les mêmes attributs et le même style //que le paragraphe précédent. //Si le paragraphe précédent est le premier impliqué dans l'alignement, //il est doté d'une 'margin-top' qu'il nous faut annuler //sur le nouveau paragraphe nodeAdded.style.removeProperty('margin-top'); //et, le cas échéant, supprimer complétement l'atribut sytle if (nodeAdded.style.cssText == '') nodeAdded.removeAttribute('style'); //On recherche l'instance tfi correpondante pour procéder à son update tfiArray.forEach(function(el){ //Sur breakme, voir http://stackoverflow.com/a/12388304 breakme: if (el.id == nodeAdded.getAttribute('data-text_floating_image')) { el.update(); break breakme; } }); } else if (nodeAdded.getElementsByTagName('IMG').length > 0) { //Ajout d'une image var imgAdded = nodeAdded.getElementsByTagName('IMG')[0]; //On détermine le bloc contenant l'image var imgAddedContainer = dom.getParent(imgAdded,dom.isBlock); //Si l'image a été insérée dans un bloc portant l'attribut //data-text_floating_image, on supprime le plugin appliqué à l'image if (imgAddedContainer.hasAttribute('data-text_floating_image')) { suppress(imgAddedContainer.getAttribute('data-text_floating_image')); } } } } //Suppression d'éléments for (var i = 0; i < mutation.removedNodes.length; i++) { if (mutation.removedNodes[i] instanceof HTMLElement) { //Déplacement d'une image //Ou suppression d'un ensemble contenant une image if (mutation.removedNodes[i].getElementsByTagName('IMG').length > 0) { var imgRemoved = mutation.removedNodes[i].getElementsByTagName('IMG')[0]; //Si l'image porte un attribut data-text_floating_image, //on supprime le plugin qui lui est appliqué if (imgRemoved.hasAttribute('data-text_floating_image')) { suppress(imgRemoved.getAttribute('data-text_floating_image')); } //Suppression d'une image seule } else if (mutation.removedNodes[i].nodeName == 'IMG') { var imgRemoved = mutation.removedNodes[i]; if (imgRemoved.hasAttribute('data-text_floating_image')) { suppress(imgRemoved.getAttribute('data-text_floating_image')); } //Suppression du dernier paragraphe lié restant } else if (mutation.removedNodes[i].hasAttribute('data-text_floating_image')) { var myId = mutation.removedNodes[i].getAttribute('data-text_floating_image'); //Si on ne trouve aucun bloc portant l'attribut data-text_floating_image, //on supprime le plugin. if (dom.select('*:not(img)[data-text_floating_image=' + myId + ']').length == 0) { suppress(myId); } } //S'il ne reste qu'un paragraphe lié ne contenant plus de texte } else { var parent = dom.getParent(mutation.target,dom.isBlock); if (parent && parent.hasAttribute('data-text_floating_image')) { if (parent.textContent.trim() == '') { var myId = parent.getAttribute('data-text_floating_image'); if (dom.select('*:not(img)[data-text_floating_image=' + myId + ']').length == 1) { suppress(myId); } } } } } }); }); //Si des instances tfi ont été créées, on déclenche la surveillance if (tfiArray.length > 0) observer.observe(body, {childList: true, subtree: true}); //On pose un écouteur sur l'événement resize de la fenêtre de l'éditeur //pour y associer la fonction resize editor.getWin().addEventListener('resize',resize); }); //On déclenche un resize dès lors que l'éditeur a fini son travail de rendu editor.on('PostProcess', function(e) { resize(); }); /*********************************************************** GESTION DU PASSAGE DU MODE VISUEL AU MODE TEXTE DE L'ÉDITEUR ************************************************************ On détecte le passage au mode texte en posant un écouteur sur l'événement click du bouton "Texte" de l'éditeur dont une rapide recherche dans l'inspecteur du navigateur révèle qu'il porte l'id "content-html". Cet événement a l'avantage d'être déclenché avant l'événement "hide" de l'éditeur et nous permet d'assurer le nettoyage de la structure html. */ $('#content-html').on('click',function(){ //On arrête la surveillance observer.disconnect(); //On assure le nettoyage des éléments de tfiArray if (tfiArray.length > 0) { tfiArray.forEach(function(el){ el.clean(); }); //On reset tfiArray tfiArray.length = 0; } }); /* Pour le retour au mode visuel, on utilise l'événement "show" de l'éditeur. À noter que cet événement n'est pas dispatché à l'ouverture de l'éditeur et qu'il n'est donc pas redondant avec l'événement "init" de ce même éditeur http://archive.tinymce.com/wiki.php/api4:event.tinymce.Editor.show */ editor.on('show', function(e) { //On recrée le tableau tfiArray dom.select('img[data-text_floating_image]').forEach(function(el){ new Tfi(el); }); resize(); //On rétablit la surveillance if (tfiArray.length > 0) observer.observe(body, {childList: true, subtree: true}); }); /* CLICK SUR LE BOUTON "METTRE À JOUR" */ editor.on('submit', function(e) { //On assure le nettoyage des éléments de tfiArray tfiArray.forEach(function(el){ el.clean(); }); editor.save(); }); /***************************************************************************** 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); //Reset du tableau selectedTextArray selectedTextArray.length = 0 //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 = doc.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 //celui-ci devient provisoirement le premier élément de selectedTextArray. //On active le bouton et on passe à la suite. for(var i = 0; i < a.length; i++){ if (a[i].textContent.trim() != '') { selectedTextArray.push(a[i]); 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.trim() != '' && 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 rng = dom.createRng(),//Objet range qui va nous permettre d'opérer //la sélection des blocs de texte liés tfi,//l'objet tfi de l'image traitée lorsu'il s'agit d'un update availableTextArray = [],//Tableau de tous les blocs de texte //susceptibles d'être impliqués dans l'alignement //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, //Variables dédiées à la construction d'availableTextArray n, temp = imgContainer; //Construction d'availableTextArray //Le tableau est constitué de tous les blocs qui suivent le container de l'image //et comportent du texte mais pas d'image //Il s'arrête dès lors qu'on trouve un bloc renfermant une image //ou ne comportant pas de texte while (n = dom.getNext(temp,dom.isBlock)) { if (n.getElementsByTagName('IMG').length == 0 && n.textContent.trim() != '') { availableTextArray.push(n); temp = n; } else break; } //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')) { //On conserve son id id = img.getAttribute('data-text_floating_image'); //On récupère l'instance tfi associée à l'image tfiArray.forEach(function(el){ breakme: if (el.id == id) { tfi = el; break breakme; } }); //Pour en faciliter la lecture, on transforme en objet //le contenu de l'attribut data-text_floating_image-style de l'image 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'); //La propriété texts de l'instance tfi renvoie un tableau //des blocs de texte participant à l'alignement //On définit donc selectedTextArray en y clonant tfi.texts //http://www.dyn-web.com/javascript/arrays/value-vs-reference.php selectedTextArray = tfi.texts.slice(0); //On place le début de notre range avant le premier bloc de texte rng.setStartBefore(tfi.first); //On cale la fin de ce même range après le dernier bloc de texte rng.setEndAfter(tfi.last); //On opère la sélection correspondante selection.setRng(rng); } else { //Première application de notre extension à une image //Calcul de l'id que nous attribuerons à l'image id = new Date().valueOf(); //Si le container de l'image ne contient pas de texte après celle-ci if (selectedTextArray.length == 0) { //On sélectionne le premier bloc de texte suivant le container de l'image var next = dom.getNext(imgContainer,dom.isBlock); selectedTextArray.push(next); selection.select(next); } else { //Sinon, on rajoute provisoirement le noeud de texte stocké dans //selectedTextArray[0] en tête d'availableTextArray //et on sélectionne tout le texte compris entre l'image et la fin de son container availableTextArray.unshift(selectedTextArray[0]); rng.setStartBefore(selectedTextArray[0]); rng.setEndAfter(imgContainer); selection.setRng(rng); } } //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 (tfi) { buttons.splice(1,0, { text: "Supprimer", classes: 'text_floating_image-suppress', subtype: 'suppress', onclick: function(){suppress(id);} } ); } //FIN DE LA CRÉATION DES BOUTONS //On enregistre une référence à notre fenêtre dans la variable myWindow myWindow = editor.windowManager.open({ onOpen: function(){ //On arrête la surveillance de l'éditeur observer.disconnect(); //Un petit tour dans l'inspecteur nous apprend que myWindow //dispose d'une propriété $el qui renvoie un tableau //dont le premier élément fait justement référence à notre fenêtre //Ne reste plus alors qu'à positionner cette dernière en haut à droite myWindow.$el[0].style.top = 0; myWindow.$el[0].style.left = 'auto'; myWindow.$el[0].style.right = 0; //On réduit l'opacité de la fenêtre modale //pour assurer une visualisation correcte de la sélection de texte //L'inspecteur nous apprend qu'elle porte un id 'mce-modal-block' $('#mce-modal-block').css('opacity',0); //La fenêtre modale bloque le scrolling sur l'éditeur //et empêche le cas échéant de visualiser le texte sélectionné //On pose donc un écouteur sur l'événement wheel //qui va nous permettre de rétablir le scroll de l'éditeur //Référence à l'iframe contenant l'éditeur var $iframe = jQuery('#content_ifr'); //Référence au contenu de l'iframe pour en obtenir la hauteur var $iframeContents = $iframe.contents(); jQuery(window).on('wheel', function(e){ //Valeur du delta appliqué au scroll var delta = 40; //Affectation du signe du delta en fonction du mouvement de la molette : //S'il s'agit d'une descente, delta est positif et on augmente la valeur de scrollTop //tant qu'on n'a pas atteint son maximum possible //Sinon, delta est négatif et on diminue la valeur de scrollTop //tant qu'il est supérieur à zéro delta = (e.originalEvent.deltaY > 0)?delta:-delta; //On stocke la valeur actuelle du scrollTop var scroll = $iframeContents.scrollTop(); //Si delta est négatif et que l'iframe n'est pas remontée au max //Ou si delta est positif et que l'iframe n'est pas descendue au max if ((delta < 0 && scroll + delta > 0) || (delta > 0 && $iframe.height() + scroll < $iframeContents.height())) { //On bloque l'événement sur l'élément window e.preventDefault(); //Et on applique le scroll sur l'iframe $iframeContents.scrollTop(scroll + delta); } }) }, onClose: function(){ //À la fermeture de la fenêtre, on sélectionne l'image selection.select(img); //On dispatch un click sur l'image img.click(); //On reset la variable myWindow myWindow = null; //On rétablit la surveillance de l'éditeur if (tfiArray.length > 0) observer.observe(body, {childList: true, subtree: true}); }, //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 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, myWindow.find('#width').value(text_floating_image_options.imageWidth); myWindow.find('#align').checked(Boolean(text_floating_image_options.alignRight)); myWindow.find('#spaceAfter').value(0); //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; //S'il n'y a qu'un bloc de texte selectionné, //on bloque le bouton boutonMoins.disabled(selectedTextArray.length == 1); }, onClick:function(){ boutonPlus.disabled(false); var length = selectedTextArray.length; //Suppression du dernier élément de selectedTextArray selectedTextArray.pop(); //Sélection du texte correspondant rng.setStartBefore(selectedTextArray[0]); rng.setEndAfter(selectedTextArray[length - 2]); selection.setRng(rng); //Si length valait 2 avant la suppresion //c'est qu'il ne reste plus qu'un élément de texte //On bloque donc le bouton boutonMoins.disabled(length == 2); }, }, { text: '+', tooltip: "Augmenter le texte aligné d'un bloc", onPostRender:function(){ boutonPlus = this; //Si tous les éléments de texte possibles sont //déjà sélectionnés, on bloque le bouton boutonPlus.disabled(availableTextArray.length == selectedTextArray.length) }, onClick:function(){ boutonMoins.disabled(false); var length = selectedTextArray.length; //On rajoute le prochain bloc de texte disponible //à selectedTextArray length = selectedTextArray.push(availableTextArray[length]); //Et on sélectionne le texte correspondant rng.setStartBefore(selectedTextArray[0]); rng.setEndAfter(selectedTextArray[length - 1]); selection.setRng(rng); //Si tous les éléments de texte possibles sont //déjà sélectionnés, on bloque le bouton boutonPlus.disabled(availableTextArray.length == length); }, } ], }, ], }, ], 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 (tfi) { tfi.preUpdate(); //Sinon } else { //On passe l'id à l'attribut data-text_floating_image de l'image img.setAttribute('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; //S'il y avait du texte après l'image au sein de son container, //le premier élément de selectedTextArray est un noeud plutôt qu'un bloc if (!dom.isBlock(selectedTextArray[0])) { //On le remplace par le bloc issu du split selectedTextArray[0] = dom.getNext(imgContainer,dom.isBlock); } } } //On passe l'id à l'attribut data-text_floating_image des blocs de texte selectedTextArray.forEach(function(el){ el.setAttribute('data-text_floating_image', id); }); //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' + ';line-height:0;'; //(En ajoutant un line-height:0, on s'assure que le paragraphe //contenant l'image aura bel et bien la hauteur pile-poil //de l'image même si celle-ci est contenue dans une ancre.) //Et on l'affecte à l'attribut data-text_floating_image-style de l'image. img.setAttribute('data-text_floating_image-style', str); //On enregistre le paramètre rentré pour le champs 'Espace après' //dans l'attibut dédié 'data-text_floating_image-after' img.setAttribute('data-text_floating_image-after', e.data.spaceAfter); if (tfi) { //S'il s'agit d'un update, on update l'instance tfi tfi.update(); } else { //Sinon, on crée l'objet tfi new Tfi(img); } } } }); //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) } function suppress(myId) { //On recherche l'instance tfi correspondante tfiArray.forEach(function(el){ breakme: if (el.id == myId) { el.suppress(); break breakme; } }); //Si la suppression est provoquée par un click sur le bouton 'Supprimer' //de la fenêtre de paramétrage, on referme la fenêtre if (myWindow) myWindow.close(); } function resize(){ if (tfiArray.length > 0) { tfiArray.forEach(function(el){ el.resize(); }); } } var Tfi = function(img){ //Affectation de l'identifiant de l'image à la propriété id this.id = img.getAttribute('data-text_floating_image'); //Référence au bloc container de l'image var imgContainer = dom.getParent(img,dom.isBlock); //Création et insertion de l'élément elastik var elastik = doc.createElement('div'); body.insertBefore(elastik,imgContainer); //Création et insertion de l'élément preElastik var preElastik = doc.createElement('div'); preElastik.style = "clear:both;"; body.insertBefore(preElastik,elastik); //Création et insertion de l'élément <style> var sheet = doc.createElement('style'); doc.head.appendChild(sheet); //On ajoute l'instance créée au tableau tfiArray tfiArray.push(this); //Méthode cleanMarges (utilisation en interne): this.cleanMarges = function () { // reset de la margin-top appliquée au premier bloc de texte if (body.contains(this.first)) { this.first.style.removeProperty('margin-top'); } // suppression de la classe attribuée au dernier bloc de texte pour // gérer son pseudo-élément ::after if (body.contains(this.last)) { this.last.classList.remove('tfi-' + this.id); if (this.last.getAttribute('class') == '') this.last.removeAttribute('class'); } } //Méthode clean appelée : // - lorsque l'on passe du mode visuel au mode texte de l'éditeur // - lorsqu'on met à jour le post // - depuis la méthode suppress de l'objet lui-même this.clean = function() { // reset des styles du premier et dernier bloc de texte via cleanMarges this.cleanMarges(); this.first.style.removeProperty('padding-top'); if (this.first.style.cssText == '') this.first.removeAttribute('style'); this.last.style.removeProperty('margin-bottom'); this.last.style.removeProperty('padding-bottom'); if (this.last.style.cssText == '') this.last.removeAttribute('style'); // supression de l'élément <style> doc.head.removeChild(sheet); // suppression des éléments preElastik et elastik if (body.contains(preElastik)) body.removeChild(preElastik); if (body.contains(elastik)) body.removeChild(elastik); // suppression de l'attribut style sur le bloc container de l'image if (body.contains(imgContainer)) imgContainer.removeAttribute('style'); //nettoyage des attributs posés sur l'image if (body.contains(img)) { img.removeAttribute('data-mce-placeholder'); if (img.hasAttribute('data-text_floating_image-classes')) { img.className = img.getAttribute('data-text_floating_image-classes'); img.removeAttribute('data-text_floating_image-classes'); } } } // Méthode preUpdate. Appelée lors d'un update // au début de la fonction 'onSubmit' de la fenêtre de paramétrage this.preUpdate = function() { // reset des styles du premier et dernier bloc de texte via cleanMarges this.cleanMarges(); //Nettoyage de margin-bottom = 0 //et padding-bottom = 0 //posés sur le dernier bloc de texte this.last.style.removeProperty('margin-bottom'); this.last.style.removeProperty('padding-bottom'); if (this.last.style.cssText == '') this.last.removeAttribute('style'); //Nettoyage de l'attribut 'data-text_floating_image-after' posé sur l'image img.removeAttribute('data-text_floating_image-after'); // retrait de l'attribut 'data-text_floating_image' // sur tous les blocs de texte affiliés this.texts.forEach(function(el){ if (body.contains(el)) el.removeAttribute('data-text_floating_image'); }); } // Méthode update. Appelée lors d'un update // à la fin de la fonction 'onSubmit' de la fenêtre de paramétrage // et en interne pour finaliser la construction de l'instance this.update = function() { // reset des styles du premier et dernier bloc de texte via cleanMarges this.cleanMarges(); // affectation des propriétés de style au bloc container de l'image imgContainer.style = img.getAttribute('data-text_floating_image-style'); // affectation de la propriété float à l'élément elastik elastik.style.float = imgContainer.style.float; // création ou reset des propriétés texts, first et last de l'instance tfi this.texts = dom.select('*:not(img)[data-text_floating_image=' + this.id + ']'); this.first = this.texts[0]; this.first.style.paddingTop = 0; this.last = this.texts[this.texts.length - 1]; this.last.style.marginBottom = 0; this.last.style.paddingBottom = 0; // construction du contenu de l'élément <style> sheet.innerHTML = ".tfi-" + this.id +"::after{content:'';display:block;clear:both;height:" + img.getAttribute('data-text_floating_image-after') + "px;}"; //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 img.setAttribute('data-mce-placeholder', 1); //On sauvegarde les classes de l'image if (img.hasAttribute('class')) { img.setAttribute('data-text_floating_image-classes', img.className); //avant de les supprimer img.removeAttribute('class'); } // appel de la méthode resize pour procéder au positionnement des éléments this.resize(); } // Méthode resize. Appelée sur l'événement resize de la fenêtre de l'éditeur // et en interne depuis la méthode update this.resize = function() { // reset de la hauteur de l'élément elastik elastik.style.height = 0; // reset des styles du premier et dernier bloc de texte via cleanMarges this.cleanMarges(); //Calcul de la hauteur des blocs de texte var h = 0; this.texts.forEach(function(el){ h += outerHeight(el); }); var imgHeight = outerHeight(img); if (imgHeight > h) { //Si l'image est plus haute que les blocs de texte //on applique une margin-top au premier bloc this.first.style.marginTop = (imgHeight - h) / 2 + 'px'; } else { //Si la hauteur totale des blocs de texte est supérieure à celle de l'image //on calcule la hauteur résultante de l'élément elastik elastik.style.height = (h - imgHeight) / 2 + 'px'; } //On applique la classe définie dans l'élément <style> //au dernier bloc de texte this.last.classList.add('tfi-' + this.id); } //Méthode suppress this.suppress = function() { // reset des styles du premier et dernier bloc de texte // via la fonction cleanMarges appelée dans la fonction preUpdate // et retrait de l'attribut 'data-text_floating_image' // sur tous les blocs de texte affiliés this.preUpdate(); //Nettoyage de padding-top = 0 //posé sur le premier bloc de texte this.first.style.removeProperty('padding-top'); if (this.first.style.cssText == '') this.first.removeAttribute('style'); // supression de l'élément <style> doc.head.removeChild(sheet); // suppression des éléments preElastik et elastik if (body.contains(preElastik)) body.removeChild(preElastik); if (body.contains(elastik)) body.removeChild(elastik); //Nettoyage de l'élément imgContainer if (body.contains(imgContainer)) { if (imgContainer.getElementsByTagName('IMG').length == 0) { //S'il ne contient plus d'image, on le supprime body.removeChild(imgContainer); } else { //Sinon, on supprime son attribut style imgContainer.removeAttribute('style'); } } //On vérifie que l'image existe toujours var myImg = dom.select('img[data-text_floating_image=' + this.id + ']')[0]; //Si c'est le cas if (myImg) { //On supprime tous les attributs ajoutés à l'image myImg.removeAttribute('data-text_floating_image'); myImg.removeAttribute('data-text_floating_image-style'); myImg.removeAttribute('data-text_floating_image-after'); myImg.removeAttribute('data-mce-placeholder'); //On lui rend ses classes myImg.setAttribute('class',img.getAttribute('data-text_floating_image-classes')); //On passe la balayette myImg.removeAttribute('data-text_floating_image-classes'); } //On supprime l'instance tfi du tableau tfiArray tfiArray.splice(tfiArray.indexOf(this), 1); //S'il reste des images traitées //on lance la fonction clean sur les instances tfi correspondantes //pour pouvoir les reconstruire sur des bases saines if (tfiArray.length > 0) { tfiArray.forEach(function(el){ el.clean(); }); } //On reset le tableau tfiArray tfiArray.length = 0; //On crée de nouvelles instance tfi toutes neuves pour les images à traiter dom.select('img[data-text_floating_image]').forEach(function(el){ new Tfi(el); }); //On reset prevEl et on déclenche un événement 'NodeChange' sur l'éditeur //pour actualiser l'état des boutons prevEl = null; editor.nodeChanged(); } //On appelle la méthode update this.update(); } /********** 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; } /* Retourne la hauteur totale d'un élément en incluant paddings, bordures et marges http://youmightnotneedjquery.com/ */ function outerHeight(el) { var height = el.offsetHeight; var style = getComputedStyle(el); height += parseInt(style.marginTop) + parseInt(style.marginBottom); return height; } }); })(); |
Félicitations à celles et ceux qui sont parvenus jusqu’ici tout en ayant méticuleusement décortiqué les près de 1000 lignes de code qui précédent. Je compte sur eux pour expliquer en détail tout le pourquoi du comment à ceux qui auront abandonné en route. Et qu’ils n’hésitent surtout pas à me taper sur les doigts pour chacune des erreurs-incohérences-aberrations (rayer les mentions inutiles et, le cas échéant, en rajouter d’autres) que leur vigilance aguerrie n’aura pas manqué de relever.
Reste qu’il est grand temps désormais de nous attaquer à la mise en place de notre effet côté front-end, ce que nous allons nous attacher à faire dans le chapitre suivant.