Exemple: une application MVC en Javascript avec un Canvas

Je vous propose de découvrir une implémentation du fameux pattern MVC en javascript. L’application suivante titrée Equipe Type permet d’ajouter des joueurs sur un terrain de football. Chaque joueur doit avoir un nom et peut se voir attribuer une note. L’affichage est effectué dans un canvas. L’utilisateur peut interagir avec les joueurs ajouter en les plaçant à sa convenance sur le terrain. Les fichiers sont relativement bien commentés et il vous sera sans nul doute facile de comprendre la logique de l’application.

Vous pouvez dès à présent tester Equipe Type sur cette page et ensuite télécharger les sources ici.

Voici une capture d’écran d’Equipe Type

Equipe Type en action

Equipe Type en action

et la structure des fichiers de cette modeste application.

structure_equipe_type

la structure d’équipe type

et les codes sources des principaux fichiers:

Tout d’abord la vue, qui a uniquement en charge l’affichage du terrain et des joueurs. Il reçoit des notifications du modèle Equipe, vous reconnaîtrez le pattern observer, et se met ainsi à jour.

view/Vue.js

function Vue(mController) {

    // model
    _this = this;
    monController = mController;

    // canvas
    canvas = null, ctx = null;
    // les modeles
    maillot = null, terrain = null;
    // test si Init();
    initTest = false;

    // on cache les input non nécessaires
    $("#addNote").hide();
    $("#addDelete").hide();
    $("#addMoreButton").hide();

    // les images
    imgMaillot = new Image();
    imgTerrain = new Image();
    imgTerrain.src = 'images/terrain_original_800_500.jpg';
    imgMaillot.src = 'images/maillot-france-50-50.png';
    imgTerrain.onload = function() {
        // quand l'image du terrain est chargée on initialise la vue
        initialisation();
    }

    function initialisation() {
        initTest = true;
        // le canvas
        canvas = document.getElementById("myCanvas");
        ctx = canvas.getContext("2d");

        // les objets
        maillot = new Maillot(imgMaillot.width, imgMaillot.height);
        terrain = new Terrain(imgTerrain.width, imgTerrain.height);
        
        // on effectue le premier affichage
        _this.update();
    }

    this.update = function(joueurs, id) {
        // on actualise l'affichage
        if (initTest == true) {
            nettoyerCanvas();
            afficherTerrain();
            if (joueurs) {
                afficherJoueurs(joueurs, id);
            }
        } else {
            initialisation();
        }
        // on actualise les boutons
        if (monController.getCurrentJoueurIndex() != null)
        {
            $("#addNote").show();
            $("#addDelete").show();

            if (monController.getCurrentJoueurIndexNote()) {
                $("#addNote").attr("value", "Modifier la Note");
            } else {
                $("#addNote").attr("value", "Noter");
            }
            $("#addButton").attr("value", "Modifier le Nom");
            $("#addMoreButton").show();
        }
        else
        {
            $("#addNote").hide();
            $("#addButton").attr("value", "Ajouter un Joueur");
            $("#addDelete").hide();
            $("#addMoreButton").hide();
        }
    }

    // les fonctions d'affichages
    var nettoyerCanvas = function() {
        ctx.clearRect(0, 0, canvas.width, canvas.height);
    }

    var afficherTerrain = function() {
        ctx.drawImage(imgTerrain, 0, 0);
    }

    var afficherJoueurs = function(joueurs, id) {
        if (joueurs) {
            for (i = 0; i < joueurs.length; i++) {
                afficherUnJoueur(joueurs[i], i, id);
            }
        }
    }

    var afficherUnJoueur = function(joueur, i, id) {
        if (joueur) {

            ctx.drawImage(imgMaillot, joueur.x, joueur.y);
            
            // on encercle le joueur s'il est sélectionné
            if (i == id) {
                ctx.beginPath();
                ctx.strokeStyle = "cyan";
                ctx.lineWidth = 15;
                ctx.arc(joueur.x + maillot.w / 2, joueur.y + maillot.h / 2, maillot.w / 1.3, 0, 2 * Math.PI);
                ctx.stroke();
            }
            if (joueur.name != "") {
                ctx.fillStyle = "black";
                ctx.font = "14pt bold Arial";
                ctx.fillText(joueur.name, joueur.x, joueur.y + maillot.h + 20);
            }

            if (joueur.note != "") {
                ctx.fillStyle = "yellow";
                ctx.font = "15pt bold sans-serif";
                ctx.fillText(joueur.note, joueur.x + maillot.w, joueur.y + maillot.h / 2);
            }
        }
    }
    
}

Le modèle: models/equipe.js

Equipe = function() {

    this.observers = new ObserverList();
    // les variables
    this.joueurs = [];
    this.currentJoueurIndex = null;

    this.addJoueur = function(joueur) {
        this.joueurs.push(joueur);
        this.currentJoueurIndex = this.joueurs.length - 1;
        this.notify(this.joueurs, this.currentJoueurIndex);
    }

    this.getJoueurId = function(i) {
        return this.joueurs[i];
    }

    this.setCurrentJoueurIndex = function(id) {
        this.currentJoueurIndex = id;
        console.log("setCurrentJoueurIndex:" + id);
        this.notify(this.joueurs, this.currentJoueurIndex);
    }

    this.getCount = function() {
        return this.joueurs.length;
    }

    this.getCurrentJoueurIndex = function() {
        return this.currentJoueurIndex;
    }

    this.getJoueurs = function() {
        return this.joueurs;
    }

    this.modifyXYofOneJoueur = function(id, donnees) {
        this.joueurs[id].x = donnees.x;
        this.joueurs[id].y = donnees.y;
        this.notify(this.joueurs, this.currentJoueurIndex);
    }

    this.reset = function() {
        this.joueurs = [];
        this.currentJoueurIndex = null;
        this.notify(this.joueurs, this.currentJoueurIndex);
    }

    this.removeAtIndex = function(i) {
        this.joueurs.splice(i, 1);
        this.notify(this.joueurs, this.currentJoueurIndex);
    }

};

Equipe.prototype.addObserver = function(observer) {
    this.observers.add(observer);
}

Equipe.prototype.removeObserver = function(observer) {
    this.observers.removeAt(this.observers.indexOf(observer, 0));
}

Equipe.prototype.notify = function(context, id) {
    var observerCount = this.observers.count();
    for (var i = 0; i < observerCount; i++) {
        this.observers.get(i).update(context, id);
    }
}

le Contrôleur, qui est interrogé par la vue pour obtenir des infos du modèle et qui informe le modèle des actions de l’utilisateur

controller/Controller.js

function Controller(mequipe) {

    monEquipe = mequipe;
    
    // gestion des actions et des clics
    var clicX, clicX;
    var drag = false;
    
    this.getCurrentJoueurIndex = function(){
        return monEquipe.getCurrentJoueurIndex();
    }
    
    this.getCurrentJoueurIndexNote = function(){
        return monEquipe.getJoueurId(monEquipe.getCurrentJoueurIndex()).getNote()
    }

    // les actions
    $("#addNew").click(function() {
        monEquipe.reset();
    });

    $("#addDelete").click(function() {
        var i = monEquipe.getCurrentJoueurIndex();

        if (i != null) {
            monEquipe.removeAtIndex(i);
            monEquipe.setCurrentJoueurIndex(null);
            $("#addButton").attr("value", "Ajouter un Joueur");
            $("#addNote").hide();
        }
        if (i == null) {
            monEquipe.reset();
        }

    });

    $("#addButton").click(function() {
        var newNom = null;
        newNom = prompt('Nouveau nom:', '');
        if (newNom != null && monEquipe.getCurrentJoueurIndex() == null) {
            joueur = new Joueur(newNom, "", 400, 200);
            monEquipe.addJoueur(joueur);
            monEquipe.setCurrentJoueurIndex(monEquipe.getCount() - 1);
        } else if (newNom != null) {
            monEquipe.getJoueurId(monEquipe.getCurrentJoueurIndex()).setName(newNom);
            _this.update(monEquipe.getJoueurs(), monEquipe.getCurrentJoueurIndex());
        }
    });

    $("#addMoreButton").click(function() {
        monEquipe.setCurrentJoueurIndex(null);
        var newNom = null;
        newNom = prompt('Nouveau nom:', '');
        if (newNom != null && monEquipe.getCurrentJoueurIndex() == null) {
            joueur = new Joueur(newNom, "", 400, 200);
            monEquipe.addJoueur(joueur);
            monEquipe.setCurrentJoueurIndex(monEquipe.getCount() - 1);
        }
    });

    $("#addNote").click(function() {
        if (monEquipe.getCurrentJoueurIndex() != null) {
            var mynote = prompt('Votre note:', '');
            var id = monEquipe.getCurrentJoueurIndex();
            var joueur = monEquipe.getJoueurId(id);
            if (mynote) {
                joueur.setNote(mynote);
                $("#addNote").attr("value", "Modifier la Note");
            }
            _this.update(monEquipe.getJoueurs(), id);
        }

    });

    // les clics sur le canvas

    $("#myCanvas").mouseup(function(event) {
        drag = false;
        $('canvas').css('cursor', 'auto');
    });

    $("#myCanvas").mousemove(function(event) {
        var currentJoueurIndex = monEquipe.getCurrentJoueurIndex();

        if (drag == true) {

            posX = event.pageX - this.offsetLeft;
            posY = event.pageY - this.offsetTop;

            newData = {};
            newData.x = posX - (maillot.w / 2);
            newData.y = posY - (maillot.h / 2);

            monEquipe.modifyXYofOneJoueur(currentJoueurIndex, newData);

        }

    });

    $("#myCanvas").mousedown(function(event) {
        clicX = event.pageX - this.offsetLeft;
        clicY = event.pageY - this.offsetTop;
        $('canvas').css('cursor', 'auto');
        if (testClicOn(clicX, clicY) == true) {
            drag = true;
            $('canvas').css('cursor', 'pointer');
        }
        else {
            drag = false;
            $('canvas').css('cursor', 'auto');
        }

    });


    // test si on clic sur un joueur 
    function testClicOn(mClicX, mClicY) {
        var lesJoueurs = monEquipe.getJoueurs();
        var test = false;
        monEquipe.setCurrentJoueurIndex(null);

        for (i = 0; i < monEquipe.getCount(); i++) {
            console.log("i:" + i);
            if (mClicX >= lesJoueurs[i].x && mClicX <= lesJoueurs[i].x + maillot.w
                    && mClicY >= lesJoueurs[i].y && mClicY <= lesJoueurs[i].y + maillot.h) {
                monEquipe.setCurrentJoueurIndex(i);
                test = true;
            } else {
                test = false;
            }
        }

        // console.log(test);
        return test;
    }
    
}

et enfin le fichier main.js, qui lance l’application

$(function() {

    monEquipe = new Equipe();
    monController = new Controller(monEquipe);
    maVue = new Vue(monController);
    // maVue sera notifiée par monEquipe de ses changements
    monEquipe.addObserver(maVue);
    
}); // end $(function(){});

Je vous laisse découvrir l’ensemble des fichiers dans le .rar. De nombreuses améliorations seraient possibles: système de sauvegarde (images, cookies, base de données, …), choix du maillot, …

Publié dans Canvas, Javascript

Tuto: un Infinite Scroll pour WordPress from Scratch

Découvrez comment réaliser un infinite scroll pour votre blog WordPress from Scratch. Ce système utilise la puissance de PHP, d’Ajax et évidemment de WP. Il affiche une liste de nouveaux billets sur la page d’accueil, les catégories, les tags et les billets. Le chargement de nouvelles entrées est lancées quand le visiteur atteint le bas de notre page. Notre infinite scroll est composé simplement d’un fichier php, qui a pour mission d’aller chercher les billets demandées par un script présent dans nos pages. Avant de vous lancer vous pouvez observer le résultat ici.

Voici tout d’abord notre fichier php, placé à la racine de notre thème.

<?php
// on inclut wp-load.php pour avoir accès à $wpdb
include("../../../wp-load.php");

$numberOfPost = 10;

$page = $_POST['id'];
$what = $_POST['type'];
$idCat = $_POST['idCat'];
$premierPostLimite = $page * $numberOfPost;

// NOS REQUETES

// single et home
if ($what == "single" || $what == "home") {
    $query = new WP_Query('posts_per_page= ' . $numberOfPost . '&amp;offset=' . $premierPostLimite);
}

// category ou tag
if (($what == "category" || $what == "tag") &amp;&amp; $idCat != "0") {
	if ($what == "category") {
        $query = new WP_Query('cat=' . $idCat . '&amp;posts_per_page= ' . $numberOfPost . '&amp;offset=' . $premierPostLimite);
    }

    if ($what == "tag") {
        $query = new WP_Query('tag_id=' . $idCat . '&amp;posts_per_page= ' . $numberOfPost . '&amp;offset=' . $premierPostLimite);
    }
}

// ON AFFICHE LES RESULTATS

while ($query->have_posts()) {
    $query->next_post();
	?>
    <li>
        <a href="<?php echo get_permalink($query->post->ID); ?>">
            <?php echo attachment_image($query->post->ID, 'thumbnail', 'alt="' . get_the_title($query->post->ID) . '"'); ?>
            <h2><?php echo get_the_title($query->post->ID); ?></h2>
            <p><small><?php echo get_the_time('j/m/Y ', $query->post->ID); ?></small></p>
        </a>
    </li>
    <?php
}
wp_reset_postdata();
?>

Voici maintenant le script présent dans le footer de notre thème. Nous déclenchons une requête Ajax, grâce à JQuery, dès que le visiteur atteint le bas de l’écran.

<script src="http://code.jquery.com/jquery-1.9.1.min.js"></script>
// ....
<script>
$(document).ready(function () {
$("#load").hide();
<?php if (is_single() || is_home() || is_category() || is_tag()) { ?>
    // nombre de chargement de l'infinite scroll
    var count = 1;
    // le script est-il en train de charger
    var isLoading = false;
    // what = single, home, category, tag ?
    var what ="";
    // on passe l'id du tag ou de la catégorie
    var catId = 0;

    // on fixe le nombre maxi de chargements en fonction du type
    <?php 
    if (is_single()) { ?> 
        var maxChargements = 10; what = "single"; 
    <?php }
    if (is_home() || is_category() || is_tag()) { ?>
        var maxChargements = 100000; 
    <?php }
    if (is_home()) { ?> 
        what = "home"; 
    <?php
    }
    if (is_category()) {
        $category = get_the_category();
        $catID = $category[0]->term_id; ?> 
        what = "category"; catId = <?php echo $catID; ?>; 
    <?php }
    if (is_tag()) { ?>
        what = "tag";
        catId = <?php echo get_query_var('tag_id'); ?>;
    <?php } ?>

    // on surveille le scroll
        $(window).scroll(function() {
            // le visiteur est en bas de la page
            if($(window).scrollTop() + window.innerHeight == $(document).height()) {
                // si on n'est pas pendant un chargement
                if(isLoading == false){
                    // si le nombre de chargement est < à maxChargements
                    if(count<maxChargements){
                        // on lance la requete ajax
                        $.ajax({
                            beforeSend: function() {
                                isLoading = true;
                                $("#load").show();
                            },
                            type: "POST",
                            url: "http://monsite.fr/wp-content/themes/montheme/infinitePost.php",
                            data: { id : count, type : what, idCat : catId },
                            success: function(res) {
                            count++;                    
                            $("#listeLastPosts").append(res);
                            $('#listeLastPosts').listview('refresh');
                        }
                        }).done(function() {
                            isLoading = false;
                            $("#load").hide();
                        });
                    }
                }
            }
        });
<?php } ?>

});

Pour pouvoir ajouter les billets récupérés, il faut évidemment que liste des billets affichés au premier chargement ait pour identifiant listeLastPosts (id=listeLastPosts). Une image pour indiquer au visiteur un chargement a quant à elle pour identifiant load (id=load). Elle est cachée au chargement de la page et affichée pendant les requêtes.
* la fonction $(‘#listeLastPosts’).listview(‘refresh’); est utilisée ici pour rafraîchir une liste JQueryMobile.

Amélioration possible et recommandée: il faudrait écrire les résultats en JSON et faire parser ces derniers par le navigateur afin d’économiser de la bande passante. Cette technique est mise en oeuvre dans le billet sur l’autocomplétion.

Publié dans Wordpress

écrivez votre CSS avec LESS pour gagner en productivité et souplesse

Si vous trouvez fastidieux la rédaction de vos mises en pages CSS, LESS va changer votre vie. Comme son nom l’indique LESS nous permet d’écrire beaucoup moins de CSS. Avec LESS, inspiré de SASS, nous écrivons des feuilles de style dynamiques. Il est par exemple possible d’importer dans une classe une classe abstraite. Nous pouvons également déclarer des constantes et les utiliser dans nos classes. Ces possibilités et d’autres, comme la possibilité de calculer des dimensions, nous font gagner en productivité, d’autant plus que l’apprentissage de LESS est très intuitif.
Avec LESS, nous écrivons des feuilles de style dont l’extension est .less. Le fichier peut-être compilé sur le serveur, via un compilateur, ou traduit en css côté client avec l’ajout d’un fichier javascript dans nos pages. Pour l’exemple suivant, j’ai choisi cette dernière solution.
Le fichier javascript est à télécharger ici: http://lesscss.org/

Voici notre feuille de styles.less

/* Importation */
@import 'styleToImport.less';

/* variables */
@blue: #17B6FF;
@rose: #EEDDFF;
@blueclair: #55DDFF;
@marginDefault: 20px;
@heightDefault: 100px;
@largeur: 25%;

/* classes abstraites */
.border-radius(@radius: 10px) {
  -moz-border-radius: @radius;
  -webkit-border-radius: @radius;
  border-radius: @radius;
}

.marginAndHeight{margin:@marginDefault; height:@heightDefault;}

/* déclarations */
body{ background: @rose; }

// les containers

#container { 
    background: @blue; 
    .border-radius; 
    .marginAndHeight;
}

#container2{ 
    background: @blueclair; 
    .border-radius(50px); 
    .marginAndHeight;}

#container3{ 
    background: @blueclair; 
    margin:@marginDefault;
    #a1{
        color:red;
    }
    #a2{
        color:orange;
    }
}

#container4{
    // operation
    width:25%+@largeur;
    background:@greenExplosif;
}

la feuille de style importée styleToImport.less :

@greenExplosif: #00FFDD;

et enfin notre page html

<!DOCTYPE html>
<html lang="fr">
    <head>
        <meta charset="utf-8"> 
        <title>Test LESS</title>
        <link rel="stylesheet/less" type="text/css" href="css/styles.less" />
        <script src="js/less-1.7.3.min.js" type="text/javascript"></script>

    </head>
    <body>

        <div id="container">
            Mon Texte
        </div>
        
        <div id="container2">
            Mon Texte Mon Texte Mon Texte Mon Texte Mon Texte Mon Texte
        </div>
        
        <div id="container3">
            <div id="a1">
                Mon Texte Mon Texte Mon Texte Mon Texte Mon Texte Mon Texte
            </div>
            
            <div id="a2">
                Mon Texte Mon Texte Mon Texte Mon Texte Mon Texte Mon Texte
            </div>
        </div>
        
        <div id="container4">
            Mon Texte Mon Texte
        </div>
    </body>

</html>

Vous pouvez observer cet exemple ici.

Pour plus d’informations, rendez-vous sur le site officiel de LESS: http://lesscss.org. Vous découvrirez notamment que LESS propose également des fonctions pour par exemple manipuler les couleurs.

lesscss

le logo de LessCss

Publié dans CSS

Afficher une nouvelle Sidebar quand la première est Scrollée

Découvrez comment simplement afficher une nouvelle sidebar quand la première est totalement scrollée et ainsi éviter de faire apparaître un horrible espace vide pendant la lecture d’un billet par l’un de vos visiteurs. Pour réussir cet effet, qui sera sans nul doute remarquer, nous allons utiliser la bibliothèque jQuery pour calculer la position de notre visiteur dans notre page et ainsi pouvoir afficher une nouvelle sidebar quand la première n’est plus visible dans la fenêtre.
Ce code n’est évidemment passe-partout, il faut adapter le principe selon vos besoins et plus particulièrement aux différents éléments composants votre page.

Voici le code testé avec les versions desktops de: Chrome Version 35.0.1916.153 m / Firefox 30.0 / Opera 22.0.1471.50 / Internet Explorer 11.0. Le test sur les tablettes et mobiles ne sont pas jugés nécessaires, une autre version sans sidebar est à préférer.
La démo est disponible ici.

<html>
<head>
<title>DEMO NOUVELLE SIDEBAR</title>
<style>
body{
margin:0;padding:0;
}
#header{
background:red;
width:100%;
display:block;
position:fixed;
height:50px;
z-index:1000;
}
h1{
text-align:center;
font-size:25px;
}
#main{
margin-top:50px;
display:block;
width:70%;
background:#F2F2F2;
float:left;
}
#sidebar{
margin-top:50px;
display:block;
width:30%;
background:#81DAF5;
float:right;
}
#secondSidebar{
position:static;
width:30%;
float:right;
background: yellow;
z-index:-1000;
}
#footer{
clear:both;
background:cyan;
}
</style>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script>
$(document).ready(function() {

$(window).scroll(function () {

// *** LES HAUTEURS ***
// la hauteur du header
var hauteurHeader = $('#header').height();
// la hauteur de la sidebar
var hauteurSidebar = $('#sidebar').height();
// la hauteur de la seconde sidebar
var hauteurSecondSidebar = $('#secondSidebar').height();
// la hauteur du main
var hauteurMain = $('#main').height();

// *** LES DISTANCES ***
// la distance scrollée depuis le haut de la fenêtre
var distanceTopWindow = $(window).scrollTop();
var distanceFooterTop = $('#footer').offset().top;

if(hauteurSidebar+hauteurSecondSidebar<hauteurMain){

if(distanceTopWindow > hauteurSidebar+hauteurHeader){
$('#secondSidebar').css({
'position': 'fixed',
'right' : 0,
'top' : hauteurHeader,
'display' : 'block'
});
}
else{
$('#secondSidebar').css({
'position' : 'static'
});
}

if(distanceFooterTop < (distanceTopWindow+hauteurSecondSidebar+hauteurHeader) ){
var topNew = distanceFooterTop-distanceTopWindow-hauteurSecondSidebar;
$('#secondSidebar').css({
'position': 'fixed',
'right' : 0,
'top' : topNew,
'display' : 'block'
});
}

}

});
});
</script>
</head>
<body>

</div>

<div id="header">

</div>

<div id="content">

<div id="main">
</div>

</div>

<div id="sidebar">
</div>

<div id="secondSidebar">
</div>

</div>

<div id="footer">
</div>

</body>
</html>
Publié dans jQuery

Tuto WordPress: une recherche avec autocomplétion from Scratch

Si vous souhaitez donner un coup de jeune au champ de recherche de votre blog WordPress avec l’autocomplétiion, il est possible en quelques lignes de code de mettre ce système en place grâce à jQuery et plus particulièrement grâce à Ajax. Tout d’abord, il faut vérifier que le champ de recherche de notre blog a bien pour id la valeur s, ce qui est le cas par défaut dans de nombreux thèmes. Ensuite, il nous faut évidemment ajouter le script jquery à nos pages. Ce dernier est généralement embarqué par de nombreux thèmes, à vous de vérifier! Notre système de recherche avec autocomplétion se compose d’un fichier PHP que nous interrogerons depuis notre page grâce à Ajax. Le fichier PHP renverra les résultats de notre recherche en JSon. Un petit bout de code javascript dans notre page transformera ces informations sous forme d’une liste, qui sera affichée dans un div sous le champ de recherche.
Avant de vous plonger dans le code, qui est très simple, vous pouvez tester votre future recherche avec autocomplétion sur le blog suivant.

Voici tout d’abord notre page html.

<!doctype html>
<html lang="fr">
<head>
// ...
<script src="http://code.jquery.com/jquery-1.10.2.js"></script>
<script>
</head>
<body>
<!-- le corps de notre page avec le champ de recherche: -->
<input type="text" value="" name="s" id="s" class="field" />
<!-- le div qui nous permettra d'afficher les résultats -->
<div id="suggestions"></div>
// ...
<script>
// la page est chargée
$(function() {
// à chaque changement de notre champ de recherche
$("#s").keyup(function () {

var value = $("#s").val();
var isLoading = false;

if(isLoading == false){
$.ajax({
beforeSend: function() {
isLoading = true;
},
type: "GET",
url: "http://monsite.fr/wp-content/themes/montheme/resultSearch.php",
data: { term : value},
success: function(res) {

$("#suggestions").empty();
var result= '';
var obj = $.parseJSON(res);
$.each(obj, function() {
if (this['url'].length == 0) {
result+= '<p>' + this['title'] + "</p>";
}else{
result+= '<p><a href="' + this['url'] + '">' + this['title'] + "</a></p>";
}
});
$("#suggestions").empty();
$("#suggestions").append(result);

}
}).done(function() {
isLoading = false;
});
}

});

});
</script>
</body></html>

Voici maintenant le fichier resultSearch.php. Il est placé dans le dossier de notre thème. Nous effectuons notre requêtes grâce à $wpdb, nous incluons donc wp-load.php. Nous utilisons la puissance de PHP pour écrire nos résultats en JSON.


<?php

// on empeche la mise en cache
header("Cache-Control: no-cache, must-revalidate"); // HTTP/1.1
header("Expires: Sat, 26 Jul 1997 05:00:00 GMT"); // Date in the past
// on fixe le content-type
header('Content-Type: text/html; charset=utf-8');
// on inclut wp-load.php pour avoir accès à $wpdb

include("../../../wp-load.php");

// on recupère la recherche
$recherche = mysql_real_escape_string($_GET['term']);

// les table de nos posts et de nos terms (=categories et =tags)
$table_name_posts = $wpdb->prefix . "posts";
$table_name_terms = $wpdb->prefix . "terms";

// si la recherche n'est pas vide
if (strlen($recherche) > 0) {

// on initialise le tableau qui sera transformé en json
$a_json = array();
$a_json_row = array();

// PREMIERE RECHERCHE TAG et CAT
// notre requete, on fixe le nombre maxi de resultats à 2
$sql = "SELECT name, term_id FROM " . $table_name_terms . " WHERE name LIKE '%$recherche%' AND term_id != 1 LIMIT 2 ";
$mesposts = $wpdb->get_results($sql);

if ($mesposts) {

// on précise que nous sommes dans les dossiers, url = "", permet d'afficher un simple et non un lien
$a_json_row["title"] = "dans les dossiers:";
$a_json_row["url"] = "";
array_push($a_json, $a_json_row);

foreach ($mesposts as $monpost) {
$linkTag = "";
$linkTag = get_tag_link($monpost->term_id);
// si le lien est vide pour un tag, on essaie avec les catégories
if ($linkTag == "") {
$linkTag = get_category_link($monpost->term_id);
}
if ($linkTag != "") {
$a_json_row["title"] = $monpost->name;
$a_json_row["url"] = $linkTag;
array_push($a_json, $a_json_row);
}
}
}

// DEUXIEME RECHERCHE DANS LES POSTS
$queryDebut = "SELECT post_title, ID FROM " . $table_name_posts . " WHERE ";

// construit la requete avec la fonction, la recherche est découpée en mots
$query = construireRequete($recherche, "post_title");

// si le résultat de construireRequete n'est pas vide, on continue
if ($query != "") {
$queryFinale = $queryDebut . " " . $query . "AND post_status = 'publish' AND post_type = 'post' ORDER BY post_date DESC LIMIT 5 ;";
$mesposts = $wpdb->get_results($queryFinale);
if ($mesposts) {

// on précise que nous sommes dans les billets
$a_json_row["title"] = "dans les billets:";
$a_json_row["url"] = "";
array_push($a_json, $a_json_row);

foreach ($mesposts as $monpost) {
$a_json_row["title"] = $monpost->post_title;
$a_json_row["url"] = get_permalink($monpost->ID);
array_push($a_json, $a_json_row);
}

} else {
$a_json_row["title"] = "pas de suggestions";
$a_json_row["url"] = "";
array_push($a_json, $a_json_row);
}
}

// si le champ de recherche est vide
} else {
$a_json_row["title"] = "entrez une recherche";
$a_json_row["url"] = "";
array_push($a_json, $a_json_row);
}

// on echo notre tableau en json
echo json_encode($a_json);

// fonction pour chercher dans les posts mot par mot

function construireRequete($recherche, $champ) {

// on prepare la requete pour les posts en decoupant la recherche en mots
$arraySearch = explode(" ", trim($recherche));
$countSearch = count($arraySearch);

// on prepare la requete
$quote = "'";

if ($countSearch > 0) {
$a = 0;
while ($a < $countSearch) {
$query = $query . $champ . " LIKE $quote%$arraySearch[$a]%$quote ";
$a++;
if ($a < $countSearch) {
$query = $query . " AND ";
}
}
} else {
$query = "";
}
return $query;
}

?>

Il est possible d’améliorer ce système en s’attaquant par exemple au css de nos résultats.

sources: http://jqueryui.com/autocomplete/ et http://www.wowww.nl/2014/02/01/jquery-autocomplete-tutorial-php-mysql/ et http://codex.wordpress.org/Class_Reference/wpdb

Publié dans jQuery, Wordpress

une Solution pour faire fonctionner Adsense avec JQuery Mobile

JQuery Mobile permet de réaliser de superbes thèmes pour les mobiles mais malheureusement le système de préchargement des pages (AJAX) bloque l’affichage des annonces Adsense. JQuery Mobile remplace en effet le contenu placé dans le bloc tagué data-role= »page » et fait ainsi dysfonctionner les script placé à l’intérieur, dont les annonces adsense. La seule solution à l’heure actuelle est de désactiver le préchargement des pages.
Voici le code à insérer dans votre header pour désactiver ce préchargement.

<html>
<head>
// ...
<script src="http://code.jquery.com/jquery-1.9.1.min.js"></script>
<script type="text/javascript">
$(document).bind("mobileinit", function () {
 $.mobile.ajaxEnabled = false; 
  });
</script>
<script src="http://code.jquery.com/mobile/1.4.2/jquery.mobile-1.4.2.min.js"></script>
// ...

Il faut impérativement placer $.mobile.ajaxEnabled = false; avant l’appel du script jquery mobile. Espérons qu’Adsense ou mieux encore JQuery Mobile proposeront prochainement une solution permettant de conserver toute la puissance de JQuery Mobile.

Publié dans Javascript, jQuery

Photoshop: tracer un cercle parfait

Il est très facile de tracer un cercle parfait dans Photoshop. Il suffit en de sélectionner l’outil pour tracer une ellipse et de maintenir la touche majuscule enfoncé.

Nous devons donc débuter par sélectionner l’outil ellipse dans le menu.

selection de l'outil ellipse dans ps

Il nous reste plus ensuite qu’à tracer notre cercle avec la touche majuscule enfoncée.

un cercle parfait dans Photoshop

un cercle parfait dans Photoshop

Publié dans Photoshop

Android: Créer des Animations

Il est possible très simplement d’animer les composants des applications Android. Avec ce guide vous pourrez en moins de cinq minutes faire réaliser des rotations, des changements de taille ou encore des translations à vos textes ou vos images et ainsi donner de la vitalité à votre application.

Tout d’abord il faut créer un dossier anim dans le répertoire res. Ensuite, il faut créer un fichier xml contenant les paramètres de notre animation. Le nom de ce fichier est libre. Nous pouvons par exemple l’appeler rotation.xml.

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <rotate android:fromDegrees="0"
        android:toDegrees="360"
        android:pivotX="50%"
        android:pivotY="50%"
        android:duration="600"
        android:repeatMode="restart"
        android:repeatCount="infinite"
        android:interpolator="@android:anim/cycle_interpolator"/>
 
</set>

Nous devons ensuite simplement créer une nouvelle animation dans une classe et appliquer cette animation à notre composant. Voici le contenu du fichier MainActivity.java.

package com.example.animation;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.Button;
import android.widget.TextView;

public class MainActivity extends Activity implements OnClickListener {
	
	Animation animationRotation;
	Button monBouton;
	TextView monTextView;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		
		monTextView = (TextView) findViewById(R.id.textView1);
		monBouton = (Button) findViewById(R.id.button1);
		
		monBouton.setOnClickListener(this);
		
		animationRotation = AnimationUtils.loadAnimation(this, R.anim.rotation);
	}

	@Override
	public void onClick(View v) {
		if(v.getId() == R.id.button1){
				monTextView.startAnimation(animationRotation);
		}
		
	}

}

Le layout pour cet exemple est le suivant: activity_main.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/LinearLayout1"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".MainActivity" >

    <TextView
        android:id="@+id/textView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/hello_world" />

    <Button
        android:id="@+id/button1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Button" />

</LinearLayout>

Android nous permet de réaliser de nombreux types d’animation. En voici la liste:

un effet fade in, inversé les valeurs de fromAlpha et de toAlpha pour réaliser un fade out

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:fillAfter="true" >
 
    <alpha
        android:duration="1000"
        android:fromAlpha="0.0"
        android:interpolator="@android:anim/accelerate_interpolator"
        android:toAlpha="1.0" />
 
</set>

faire clignoter votre texte

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <alpha android:fromAlpha="0.0"
        android:toAlpha="1.0"
        android:interpolator="@android:anim/accelerate_interpolator"
        android:duration="600"
        android:repeatMode="reverse"
        android:repeatCount="infinite"/>
</set>

un effet zoom

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:fillAfter="true" >
 
    <scale
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:duration="1000"
        android:fromXScale="1"
        android:fromYScale="1"
        android:pivotX="50%"
        android:pivotY="50%"
        android:toXScale="3"
        android:toYScale="3" >
    </scale>
 
</set>

une rotation

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <rotate android:fromDegrees="0"
        android:toDegrees="360"
        android:pivotX="50%"
        android:pivotY="50%"
        android:duration="600"
        android:repeatMode="restart"
        android:repeatCount="infinite"
        android:interpolator="@android:anim/cycle_interpolator"/>
 
</set>

une translation

<?xml version="1.0" encoding="utf-8"?>
<set
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:interpolator="@android:anim/linear_interpolator"
    android:fillAfter="true">
 
   <translate
        android:fromXDelta="0%p"
        android:toXDelta="75%p"
        android:duration="800" />
</set>

un slide in, et un slide out en inversant les valeurs de fromXScale et de fromYScale

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:fillAfter="true">
 
    <scale
        android:duration="500"
        android:fromXScale="1.0"
        android:fromYScale="0.0"
        android:interpolator="@android:anim/linear_interpolator"
        android:toXScale="1.0"
        android:toYScale="1.0" />
 
</set>

un effet bounce

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:fillAfter="true"
    android:interpolator="@android:anim/bounce_interpolator">
 
    <scale
        android:duration="500"
        android:fromXScale="1.0"
        android:fromYScale="0.0"
        android:toXScale="1.0"
        android:toYScale="1.0" />
 
</set>
Publié dans Android

Android: personnaliser les menus et gérer les clics

L’affichage et la personnalisation d’un menu dans une application Android est très simple. Nous devons simplement indiquer les items de notre menu dans un fichier xml dans le dossier res/menu/ et appeler son affichage dans la classe main. La gestion des actions sur notre menu est également implémenter dans cette classe.

Voici les fichiers et les quelques lignes de code à mettre en place pour afficher un menu et gérer les clics sur les items de ce dernier.

le fichier res/menu/main.xml

<menu xmlns:android="http://schemas.android.com/apk/res/android" >

    <item
        android:id="@+id/option_menu_1"
        android:title="Option 1"/>
    <item
        android:id="@+id/option_menu_2"
        android:title="Option 2"/>

</menu>

le fichier MainActivity.java où le fichier main.xml est « inflate » dans la fonction onCreateOptionsMenu et où nous gérons simplement les actions sur ce dernier avec la fonction onOptionsItemSelected.

package com.example.testsdivers;

import android.app.Activity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;

public class MainActivity extends Activity  {


	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);

	}

	@Override
	public boolean onCreateOptionsMenu(Menu menu) {
		getMenuInflater().inflate(R.menu.main, menu);
		return true;
	}

	@Override
	public boolean onOptionsItemSelected(MenuItem item) {
		switch (item.getItemId()) {
		case R.id.option_menu_1:
			Toast.makeText(this,"Option Menu 1",Toast.LENGTH_SHORT).show();
			return true;
		case R.id.option_menu_2:
			Toast.makeText(this,"Option Menu 2",Toast.LENGTH_SHORT).show();
			return true;
		default:
			return true;
		}
	}

}

android menu

Android menu après un clic

Publié dans Android

Android : Afficher une Activity sous la forme d’une Boîte de Dialogue

Pour afficher une activité Android sous la forme d’une boîte de dialogue, il suffit simplement dans le manisfest d’appliquer le thème par défaut Dialog à notre activité. Cette déclaration s’effectue ainsi: android:theme= »@android:style/Theme.Dialog ».

Voici les fichiers à mettre en place pour arriver au résultat suivant.

Afficher une Activity sous la forme d'une Boîte de Dialogue

Afficher une Activity sous la forme d’une Boîte de Dialogue

Le fichier MainActivity, qui permet de lancer l’activité ActivityDialog après un clic sur un bouton.

package com.example.testsdivers;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;

public class MainActivity extends Activity implements OnClickListener  {

	Button bouton1;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);

		bouton1 = (Button) findViewById(R.id.button1);
		bouton1.setOnClickListener(this);

	}

	@Override
	public void onClick(View v) {
		if(v.getId() == R.id.button1){
			Intent i = new Intent(this,ActivityDialog.class);
			startActivity(i);		
		}
	}

}

le fichier ActivityDialog.java

package com.example.testsdivers;

import android.app.Activity;
import android.os.Bundle;

public class ActivityDialog extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // TODO Auto-generated method stub
        super.onCreate(savedInstanceState);
        setContentView(R.layout.about);
    }

}

le layout activity_main.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".MainActivity" >

    <Button
        android:id="@+id/button1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentTop="true"
        android:text="Button" />

</RelativeLayout>

le layout about.xml

<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:padding="10dip" >
  <TextView android:id="@+id/about_content"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="Mon Texte"/>
</ScrollView>

et enfin le manifest

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.testsdivers"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="8"
        android:targetSdkVersion="18" />

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name="com.example.testsdivers.MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity
            android:name=".ActivityDialog"
            android:label="About label"
            android:theme="@android:style/Theme.Dialog" >
        </activity>
    </application>

</manifest>
Publié dans Android

quand j’aurai le temps

  • les filtres wordpress
  • plugin wordpress et enregistrement de données
  • les wordpress custom post type
  • la bdd d'un blog wordpress
  • la balise more de wp
  • personnaliser une galerie wp
  • gérer les longueurs des extraits de wp
  • les animations css3
  • le memento symphony2
  • le squelette d'une page html5
  • liste sur plusieurs colonnes
  • le responsive design
  • exemple d'un jeu basique en html5
  • la réplication des bases de données
  • mettre en place une architecture en silo avec wp
  • parser un fichier xml (donc un rss) avec php5
  • mettre en place lightbox sans plugin
  • améliorer les performances de son wp
  • ajouter un bouton à l'éditeur de texte de wp
  • ...