Pour illustrer la metabox du jour, le mieux est de se mettre en situation…
Imaginons que nous réalisons un site WordPress pour un établissement qui organise des colloques et des conférences. Sur ce site, mon client souhaiterai y présenter les évènements, en détail, ainsi que les intervenants.
On a plein de données à renseigner pour l’un comme pour l’autre. Nous allons donc créer deux types de contenus.
Mais bien sûr il va falloir les lier, pour dire quel conférencier participe à quel colloque. Il nous faut une metabox !Problème : Il y a plusieurs conférenciers pour chaque évènement, je ne peut donc pas utiliser un champ select pour faire la liaison. Je ne peux pas non plus utiliser des cases à cocher car j’ai environ 150 intervenants dans ma base, ça va faire un paquet de checkbox…
L’idéal serait une metabox avec un champ de recherche pour trouver les conférenciers. Lorsque l’on trouverait celui qui nous intéresse nous pourrions l’ajouter à une liste qui sera sauvegardée dans une metadonnée du colloque.
Avant de se jeter dans le code, commençons par faire l’inventaire de ce dont nous allons avoir besoin.
Comme toujours, il faudra commencer par les fonctions habituelles d’initilisation/construction/sauvegarde de la meta box
Pour le champ de recherche, un système d’autocomplétion serait parfait. On commencerait à saisir le nom d’un conférencier qu’il apparaitrait dans le champ. Un clic dessus nous permettrai de l’ajouter à une liste ainsi qu’a un champ caché. Pour le système d’autocomplétion, je propose jquery-ui-autocomplete.
Il nous faut donc aussi un tableau javascript de tous les intervenants potentiels…
… et un champ caché …
… et une liste non-ordonnée, en HTML, pour faire un retour visuel à l’utilisateur
Qui dit HTML dit aussi un peu de css pour la mise en forme
Pour finir bah il reste le gros javascript de bœuf. On va s’inspirer de celui-ci pour le faire évoluer selon nos besoins.
Le code PHP
Notre metabox va reposer à 100% sur jquery autocomplete, il faut donc l’ajouter à l’admin WordPress, sans oublier les styles qui vont bien.
Le code suivant est à ajouter au fichier functions.php (vous trouverez le style ici). Pensez à mettre à jour les chemins…
Maintenant qu’on a mis en place la librairie, nous pouvons attaquer la metabox.
J’initialise…
// Initialisation de la metabox, pour le CPT "conference" add_action('add_meta_boxes','mes_metaboxes'); function mes_metaboxes(){ add_meta_box('conferenciers_presents', 'Conférenciers présents', 'conferenciers_concernes', 'conference', 'side', 'default'); }
//Construction function conferenciers_concernes($post){ //je récupère la meta potentiellement sauvegardée $conferenciers_presents = get_post_meta($post->ID,'_conferenciers_presents',false);
//je créer un nonce wp_nonce_field('update-conferenciers_'.$post->ID, '_wpnonce_update_conferenciers);
//mon widget echo '<div class="ui-widget">'; // le champ qui servira de support à autocomplete echo '<label for="nom">Nom : </label><input id="nom" type="text" />'; // la liste des conférenciers concernés (assurant un retour visuel pour l'utilisateur) echo '<ul>'; // j'y affiche toutes les entrées déjà sauvegardées dans la meta</ul> if( ! empty( $conferenciers_presents) ) foreach( $conferenciers_presents as $c ) echo '<li data-id="' . $c . '"><span class="erase">x</span> ' . get_the_title($c) . '</li> echo '<ul>';
// mon champ caché, que je mettrai à jour et sauvegarderai // il contient déjà les valeurs de la meta echo'<input id="conf_presents" type="hidden" name="conf_presents" value="'.implode(',',$conferenciers_presents).'" />';
//sauvegarde add_action('save_post','sauvegarde_metabox'); function sauvegarde_metabox($post_id){ //s'il s'agit bien d'une sauvegarde volontaire if( ( !defined( 'DOING_AJAX' ) || !DOING_AJAX ) && isset($_POST['conf_presents'])){ //test du nonce check_admin_referer( 'update-conferenciers_'.$post_id,'_wpnonce_update_conferenciers' );
// je suprrime tout delete_post_meta($post_id,"_conferenciers_presents"); //j'éclate mon input $conf = explode(',',$_POST['conf_presents']); foreach($conf as $c){ //pour chaque entrée j'ajoute une meta add_post_meta($post_id, "_conferenciers_presents", intval($c)); } } }
Pour l’instant notre meta box ne fait rien du tout. Pour cause on vient juste d’en poser les fondations ; rien en sera actif sans le javascript.
3 tonnes de javascript
On arrive à la grosse partie. Avant toute chose, autocomplete va avoir besoin d’une liste des éléments qui peuvent être appelés. Ce qu’on veut c’est lister tous les intervenants dans la base.
Il faut donc créer un tableau en js, et le remplir via une boucle WordPress renvoyant la totalité des conférencier.
Créer une balise <script> dans la metabox, et ajouter ici ce code :
//no-conflict jQuery(function($) {
// un tableau avec tous les conférenciers que l'on peut sélectionner var availableTags = <?php $confe = get_posts('post_type=conferencier&posts_per_page=-1'); foreach($confe as $cf){ echo '{value:"'.$cf->ID.'",label:"'.esc_js($cf->post_title).'"},'."\n"; }?> });
On a maintenant tout ce qu’il nous faut pour coder l’ajout d’un intervenant, à savoir : un tableau qui donne pour chaque conférencier son nom et son ID
L’ajout d’un intervenant
Toujours dans la même balise <script>, on conçoit la fonction d’autocomplétion.
//autocomplete sur le champ #nom $( "#nom" ).autocomplete({ // je mets le tableau précédemment crée en source source: availableTags, // lorsqe l'on sélectionne un élément select: function(event,ui){ //je crée un nouveau <ul> <li>var li = '</li> <li data-id="' + ui.item.value + '"><span class="erase">x</span> ' + ui.item.label + '</li> <li>'; //je fais un tableaux des conférencier déjà ajouté var all_conf_presents = new Array(); all_conf_presents =($('#conf_presents').val()!='') ? $('#conf_presents').val().split(',') : []; // si il est déjà dans la liste, j'en tiens pas compte if($.inArray(ui.item.value,all_conf_presents)!="-1"){ $(this).val(''); }else{ //sinon je l'ajoute à la liste all_conf_presents.push(ui.item.value); //je pousse cette liste dans le champ caché $('#conf_presents').val(all_conf_presents); //et j'ajoute la nouvelle entrée dans le <ul> var $cp= $( "#conferenciers_presents" ); $cp.append(li); $(this).val('');</ul> } //juste pour que la sélection d'un élément ne remplisse pas le input (comportement normal) return false; } });
Voilà, dès maintenant la meta box nous permet d’ajouter des entrées. Pas de problème avec la sauvegarde non plus. Par contre il n’est toujours pas possible de supprimer un élément.
Suppression d’un conférencier
Encore et toujours dans la même balise <script>, à la suite du reste, on ajoute le code ci-dessous qui va nous permettre de retirer/dissocier un intervenant de l’événement.
L’écouteur d’événement est contenu dans fonction, de façon à pouvoir le re-associer facilement aux éléments qui seront ajoutés par la suite (j’aime pas la fonction “live” de jQuery et j’assume…).
//function qui me sert à supprimer l'ID d'un conférencier dans #conf_presents function removeByElement(arrayName,arrayElement){ for(var i=0; i if(arrayName[i]==arrayElement) arrayName.splice(i,1); } }
//évènement de suppression de conférencier function listenerremove(){ $( "#conferenciers_presents" ).find('li .erase').on('click',function(){ // suppression élément var $elem = $(this).parent('li'); //je cible l'élément à supprimer //je construit un talbeau avec les conférencier actuellement liés var all_conf_presents = new Array(); all_conf_presents =$('#conf_presents').val().split(','); //je récupère l'ID à retirer var dataval = $elem.attr('data-id'); // je supprime l'ID du tableau removeByElement(all_conf_presents,dataval); //je supprime le conférencier dans la liste $elem.remove(); //je supprime son ID dans le champ caché $('#conf_presents').val(all_conf_presents); }); }
Il ne nous reste plus qu’à ajouter un peu de CSS pour style le bouton de suppression. Dans la fonction de construction de la meta box, ajouter une balise <style>, et collez-y ces styles :
Vous devriez maintenant obtenir ce résultat, dans votre fichier “functions.php” :
// chargement des scripts add_action('admin_enqueue_scripts', 'my_admin_scripts_method'); function my_admin_scripts_method(){ if(is_admin()){ wp_enqueue_script('jquery-ui-autocomplete'); wp_register_style('jquery.ui.theme', get_bloginfo('template_url') . '/css/jquery-ui-1.8.19.custom.css'); wp_enqueue_style('jquery.ui.theme'); } }
// Initialisation de la metabox, pour le CPT "conference" add_action('add_meta_boxes','mes_metaboxes'); function mes_metaboxes(){ add_meta_box('conferenciers_presents', 'Conférenciers présents', 'conferenciers_concernes', 'conference', 'side', 'default'); }
//Construction function conferenciers_concernes($post){ $conferenciers_presents = get_post_meta($post->ID,'_conferenciers_presents',false);
// un tableau avec tous les conférenciers que l'on peut sélectionner var availableTags = [<?php $confe = get_posts('post_type=conferencier&posts_per_page=-1'); foreach($confe as $cf){ echo '{value:"'.$cf->ID.'",label:"'.esc_js($cf->post_title).'"},'."\n"; } ?>];
//autocomplete sur le champ #nom $("#nom").autocomplete({ source: availableTags, select: function(event,ui){ var li = '<li data-id="' + ui.item.value + '"><span class="erase">x</span> ' + ui.item.label + '</li>'; var all_conf_presents = new Array(); all_conf_presents =($('#conf_presents').val()!='') ? $('#conf_presents').val().split(',') : []; if($.inArray(ui.item.value,all_conf_presents)!="-1"){ $(this).val(''); }else{ all_conf_presents.push(ui.item.value); $('#conf_presents').val(all_conf_presents); var $cp= $( "#conferenciers_presents" ); $cp.append(li); $(this).val(''); listenerremove(); }
return false; } });
//function qui me sert à supprimer l'ID d'un conférencier dans #conf_presents function removeByElement(arrayName,arrayElement){ for(var i=0; i<arrayName.length;i++ ){ if(arrayName[i]==arrayElement) arrayName.splice(i,1); } }
//évènement de suppression de conférencier function listenerremove(){ $("#conferenciers_presents").find('li .erase').on('click',function(){ var $elem = $(this).parent('li'); var all_conf_presents = new Array(); all_conf_presents =$('#conf_presents').val().split(','); var dataval = $elem.attr('data-id'); removeByElement(all_conf_presents,dataval); $elem.remove(); $('#conf_presents').val(all_conf_presents); }); }
Notre meta box de liaison inter-contenus est opérationnelle à 100%. Grâce à elle on pourra faire ressortir les conférenciers associé à chaque colloque, lorsque l’on sera sur la page de la conférence, grâce à la fonction get_post_meta().
Nous pourrons aussi, sur la page du conférencier, lister ces interventions, via une requête personnalisée du style :
Pas mal, mais le plus simple est d’utiliser un plugin éprouvé de type post 2 posts : http://wordpress.org/extend/plugins/posts-to-posts/ codé par Nacin et gère beaucoup de choses comme la cardinalité des liaisons, la possibilité de faire directement les requêtes dans la WP_Query, des métas etc etc.. Le mieux est d’aller voir dans la documentation https://github.com/scribu/wp-posts-to-posts/wiki qi a une partie pour les développeurs. Sachant qu’il est bien écrit et niveau performances c’est assez puissant. On peut même taper dans son API et ses méthodes de classe . Ca permet même de lier un post type avec des users WordPress 😉
Ouai il à l’air pas mal du tout ce petit plugin ! Merci pour sa présentation
Après l’article est plus dans l’esprit “Do it Yourself” C’est pas que j’aime pas les plugins (bien au contraire ^^) mais je n’ai pas toujours tout le temps besoin de toutes les fonctionnalités du truc…
Sinon c’est sympa aussi de voir comment ça marche 😉
Mais t’as raison, pour des utilisation avancés, ton plugin fera gagner du temps !
Oui oui parfois les différentes fonctionnalités ne sont pas toutes nécessaire, par contre ce plugin est très simple et ne fait pas dans les fioritures folles, on peut faire une liaison et la caractériser avec des métas.
Je suis totalement d’accord que parfois le développement perso est largement suffisant ;), surtout si on veut packager notre fonctionnalité avec le thème :).
Dans l’ensemble il vaut mieux utiliser un plugin de ce type qui est codé de façon magistrale et surtout par un core dev de WordPress :). La richesse de son wiki et de la doc montre qu’il y a une
mise à jour de façon récurrente. J4ai posté un problème sur le github et j’ai eu une réponse dans la journée 🙂
Hello
Je suis de l’avis de Raherian pour une véritable utilisation MAIS j’adore encore plus fouioller le code et réussir à faire ça à la main, avec2 bouts de ficelle (bon faut que ça reste propre et un minimum opti).
Encore bravo, tu as dû passer un sacré bout de temps pour tous ces tutos meta box (c’est pas fini peut etre ?? :p)
Petit détail (que tu avais aussi fait dans ton précédent tuto) c’est que tu testes « if($conferenciers_presents[0]!=NULL) » mais si « $conferenciers_presents[0] » n’existe pas du tout (il peut exister et valoir null) alors une notice apparait ! Il te faut tester avec isset() : « if( isset( $conferenciers_presents[0] ) ) » ou avec count() « if( count( $conferenciers_presents ) > 0 ) ».
Je vais essayer de reproduire le tuto car il me semble que WP intègre un système qu’on peut détourner pour faire la recherche d’un post en ajax au lieu de tout intégrer mes 15000 conférienciers :p, il l’utilise dans la page des médias.
Je te trouver ça 😉
: oui i lfaut regarder du côté du fichier wp-adminjsnav-menu.dev.js. Je l’ai utilisé pour la recherche avec relation post types, c’est dans la fichier js du plugin ;). C’est assez simple et bien fait 🙂
Non je pensais à $.findPosts() dans media.dev.js couplé avec find_posts_div() de WP couplé encore à du ob_start() avec callback pour replace du contenu.
C’est un poil plus complexe, mais à l’arrivée je ne fais aucune requête de tous mes posts pour les mettre dans un ui-autocomplete, à la place, je fais des requêtes ajax selon le texte frappé, bien plus léger donc je pense.
Non ?
Je dois faire une démo ??
Je viens de tester en vrai et ça ne fonctionne pas, on dirait que tu modifies ton code dans l’éditeur de WordPress sans tester, car là, tu as pas pu le faire fonctionner comme ça, je corrige juste une ligne mais qui sans elle, ne supprime pas les éléments :
puisque #conf_present est un INPUT est non pas une DIV 😉
Aussi, correction sécu+fonctionnelle : Ne jamais afficher des infos sans les sanitizer !
Voici la modif :
var availableTags = [ID.'",label:"'.$cf->post_title.'"},'."n";
}
?>];
devient :
var availableTags = [ID.'",label:"'.esc_js($cf->post_title).'"},'."n"; ////
}
?>];
J’ai ajouté « esc_js() » qui est là pou « escaper » du contenu pour le « js ».
Fait le test avec un conférencier qui contient une double quote (« ) dans son nom, et boum, tu te retrouves avec de la bad concat en JS, forcément ça pète 😉
J’ai supprimé le order by title car on s’en fiche bien gras, et en plus, le tri sur une chaine est moins bon en perf que sur une date (me trompe-je Rahe ?)
Et comme j’ai dis au dessus :
if($conferenciers_presents[0]!=NULL){
devient
if( isset( $conferenciers_presents[0] ) ){
Et pour info, j’ai du ne pas mettre le CSS perso car ça me fait un carré voir avec texte en noir 😮
Bravo pour ce tuto tout de même, par contre, je pense qu’un débutant sera perdu car tu dis « créez une balise SCRIPT dans la meta box », mais c’est quoi la metabox ? je la mets ou ? etc, je te conseille de donner le code complet, final à utiliser. En ajoutant bien sur des recommandations comme « mon custom post type est ‘conférencier’ à vous de mettre le votre ».
Dans tous les cas, pour un dev WP, ça psoe pas de soucis, j’ai réussi à le faire fonctionner ;p
Allez je jete un oeil pour mon find_posts_div() 😉
Oui effectivement, tu m’as démasqué, j’ai modifié mon code sans le tester ^^ désolé.
Je suis conscient que ce ne doit pas être très clair pour les débutants, j’espère progresser dans la rédaction des prochains articles 🙂 (ainsi qu’en sécu^^)
Et sinon je tiens à dire merci pour vos remarques !!
Pour les requêtes en ajax avec $.findPosts() je vais tester, ne t’embêtes pas à faire une démo 🙂
j’ai mis l’article à jour en tenant compte de tes remarques de code.
Seul petit détail, pour le test :
if( isset( $conferenciers_presents[0] ) ){
//...
j’ai préféré faire :
if( !empty( $conferenciers_presents ) ){
//...
Je pense que ça revient au même.
Sinon c’est le cinquième article sur les metabox que je fais. C’est juste pour moi l’occasion de partager des bouts de code que j’utilise pas mal au quotidien.
Des fois des plugins peuvent faire la même chose, et je réinvente sans doute la roue… mais au moins je sais ce qu’ils ont dans le ventre, et ils font exactement ce dont j’ai besoin. Ils ne m’ajoutent pas des dizaines d’options (ce que j’apprécie fortement ;-)) et quelque part je trouve ça plus clair pour mes clients.
J’ai prévu un dernier article dans la série mais j’annonce la couleur : ça fera exactement la même chose que “next gen gallery” mais en plus simple… J’vais pas le faire tout de suite car je pars en vacances, mais à mon retour 🙂
Après ça sera l’occaz’ de parler d’autres choses que de metabox
en attendant je vais aller jeter un oeil du côté de “media.dev.js”
Ok, pour empty(), ça marche dans ton cas, mais attention car elle retourne faux si la variable testé evaut 0 ou « 0 » ou « » ou FALSE ou array(), alors que isset() renvoie vrai pour tout ça.
(empty() renvoie vrai aussi pour NULL et pour une variable déclarée mais non ‘remplie’)
=> empty() ne vaut pas isset() 😉
Bertrand, il y a 12 ans
Salut Willy, Merci pour ce tuto qui m’a grandement aidé.
Je ne sais pas si tu l’as remarqué mais lors de la sauvegarde si aucun conférencier n’est entré le nom de l’article est automatiquement enregistré comme conférencier. Je tente depuis plusieurs heure de régler cela mais sans succès
merci pour cet article et merci surtout pour les commentaires… Je viens d’apprendre la différence entre !empty et isset alors que j’ai toujours été persuadé que la fonction faisait la même chose.
Et félicitation pour ton blog, c’est rare de tomber sur un design si abouti.
Après l’article est plus dans l’esprit “Do it Yourself” C’est pas que j’aime pas les plugins (bien au contraire ^^) mais je n’ai pas toujours tout le temps besoin de toutes les fonctionnalités du truc…
Sinon c’est sympa aussi de voir comment ça marche 😉
Mais t’as raison, pour des utilisation avancés, ton plugin fera gagner du temps !
Je suis totalement d’accord que parfois le développement perso est largement suffisant ;), surtout si on veut packager notre fonctionnalité avec le thème :).
Dans l’ensemble il vaut mieux utiliser un plugin de ce type qui est codé de façon magistrale et surtout par un core dev de WordPress :). La richesse de son wiki et de la doc montre qu’il y a une
mise à jour de façon récurrente. J4ai posté un problème sur le github et j’ai eu une réponse dans la journée 🙂
Je suis de l’avis de Raherian pour une véritable utilisation MAIS j’adore encore plus fouioller le code et réussir à faire ça à la main, avec2 bouts de ficelle (bon faut que ça reste propre et un minimum opti).
Encore bravo, tu as dû passer un sacré bout de temps pour tous ces tutos meta box (c’est pas fini peut etre ?? :p)
Petit détail (que tu avais aussi fait dans ton précédent tuto) c’est que tu testes « if($conferenciers_presents[0]!=NULL) » mais si « $conferenciers_presents[0] » n’existe pas du tout (il peut exister et valoir null) alors une notice apparait ! Il te faut tester avec isset() : « if( isset( $conferenciers_presents[0] ) ) » ou avec count() « if( count( $conferenciers_presents ) > 0 ) ».
Je vais essayer de reproduire le tuto car il me semble que WP intègre un système qu’on peut détourner pour faire la recherche d’un post en ajax au lieu de tout intégrer mes 15000 conférienciers :p, il l’utilise dans la page des médias.
Je te trouver ça 😉
C’est un poil plus complexe, mais à l’arrivée je ne fais aucune requête de tous mes posts pour les mettre dans un ui-autocomplete, à la place, je fais des requêtes ajax selon le texte frappé, bien plus léger donc je pense.
Non ?
Je dois faire une démo ??
devient
puisque #conf_present est un INPUT est non pas une DIV 😉
Aussi, correction sécu+fonctionnelle : Ne jamais afficher des infos sans les sanitizer !
Voici la modif :
devient :
J’ai ajouté « esc_js() » qui est là pou « escaper » du contenu pour le « js ».
Fait le test avec un conférencier qui contient une double quote (« ) dans son nom, et boum, tu te retrouves avec de la bad concat en JS, forcément ça pète 😉
J’ai supprimé le order by title car on s’en fiche bien gras, et en plus, le tri sur une chaine est moins bon en perf que sur une date (me trompe-je Rahe ?)
Et comme j’ai dis au dessus :
devient
Et pour info, j’ai du ne pas mettre le CSS perso car ça me fait un carré voir avec texte en noir 😮
Bravo pour ce tuto tout de même, par contre, je pense qu’un débutant sera perdu car tu dis « créez une balise SCRIPT dans la meta box », mais c’est quoi la metabox ? je la mets ou ? etc, je te conseille de donner le code complet, final à utiliser. En ajoutant bien sur des recommandations comme « mon custom post type est ‘conférencier’ à vous de mettre le votre ».
Dans tous les cas, pour un dev WP, ça psoe pas de soucis, j’ai réussi à le faire fonctionner ;p
Allez je jete un oeil pour mon find_posts_div() 😉
devient
j’ai ajouté « n » aussi pour que dans le source on y voit clair :p
Je suis conscient que ce ne doit pas être très clair pour les débutants, j’espère progresser dans la rédaction des prochains articles 🙂 (ainsi qu’en sécu^^)
Et sinon je tiens à dire merci pour vos remarques !!
Pour les requêtes en ajax avec $.findPosts() je vais tester, ne t’embêtes pas à faire une démo 🙂
Seul petit détail, pour le test :
j’ai préféré faire :
Je pense que ça revient au même.
Sinon c’est le cinquième article sur les metabox que je fais. C’est juste pour moi l’occasion de partager des bouts de code que j’utilise pas mal au quotidien.
Des fois des plugins peuvent faire la même chose, et je réinvente sans doute la roue… mais au moins je sais ce qu’ils ont dans le ventre, et ils font exactement ce dont j’ai besoin. Ils ne m’ajoutent pas des dizaines d’options (ce que j’apprécie fortement ;-)) et quelque part je trouve ça plus clair pour mes clients.
J’ai prévu un dernier article dans la série mais j’annonce la couleur : ça fera exactement la même chose que “next gen gallery” mais en plus simple… J’vais pas le faire tout de suite car je pars en vacances, mais à mon retour 🙂
Après ça sera l’occaz’ de parler d’autres choses que de metabox
en attendant je vais aller jeter un oeil du côté de “media.dev.js”
(empty() renvoie vrai aussi pour NULL et pour une variable déclarée mais non ‘remplie’)
=> empty() ne vaut pas isset() 😉
Je ne sais pas si tu l’as remarqué mais lors de la sauvegarde si aucun conférencier n’est entré le nom de l’article est automatiquement enregistré comme conférencier. Je tente depuis plusieurs heure de régler cela mais sans succès
merci pour cet article et merci surtout pour les commentaires… Je viens d’apprendre la différence entre !empty et isset alors que j’ai toujours été persuadé que la fonction faisait la même chose.
Et félicitation pour ton blog, c’est rare de tomber sur un design si abouti.
Par contre le lien pour télécharger les style semble mort, est-il possible de ré-hébergé le fichier ?
Merci
et bonne continuation pour cet excellent site.
Hélo