blog.pagesd.info

Aller au contenu | Aller au menu | Aller à la recherche

jeudi 16 juin 2011

Utiliser jQuery UI Datepicker avec ASP.NET MVC

Normalement, je n'aime pas trop proposer un calendrier pour permettre la saisie de dates dans un formulaire. Mais comme le widget Datepicker de jQuery UI n'est pas trop envahissant ou irritant, j'ai malgré tout décidé de lui laisser une chance. Et surtout, je vois ça comme un bon moyen d'aborder les templates Editor et Display de ASP.NET MVC et d'étudier un peu plus correctement comment les utiliser.

Un projet MVC juste pour tester

Pour commencer, j'ai échafaudé vite fait une application de gestion de livres à partir de la classe suivante :

public class Livre
{
    public int ID { get; set; }
    public string Titre { get; set; }
    public DateTime Edition { get; set; }
}

Pour faire vite, j'ai simplement aménagé l'exemple EF Code First and Data Scaffolding with the ASP.NET MVC 3 Tools Update de Scott Guthrie. Après avoir bricolé un lien vers l'action "Index" de mon nouveau contrôleur "Livres" dans le fichier "_Layout.cshtml", j'ai pu accéder à mon écran de création d'un nouveau livre :

jquery-datepicker-mvc-01.jpg

Deux templates DateTime.cshtml

J'ai alors pu passer à l'exemple Create user friendly date fields with ASP.NET MVC EditorTemplates & jQueryUI donné par Rachel Appel pour créer les deux templates "DateTime.cshtml" dans les sous-répertoires "DisplayTemplates" et "EditorTemplates".

\Views\Shared\DisplayTemplates\DateTime.cshtml

@model DateTime   
@String.Format("{0:d}", Model.Date)

\Views\Shared\EditorTemplates\DateTime.cshtml

@model DateTime
@Html.TextBox("", String.Format("{0:d}", Model.Date.ToShortDateString()))

Ce qui m'a permi d'avoir des dates "propres" (sans avoir l'heure à 00:00:00) que ce soit en affichage ou en saisie :

jquery-datepicker-mvc-02.jpg

L'avantage, c'est que maintenant je n'ai rien d'autre à faire pour que toutes les zones de dates existantes ou à venir dans mon application Livres bénéficient de la même présentation.

Ajout de jQuery UI Datepicker

Je continue pas à pas le tutoriel de Rachel Appel en référençant la librairie jQuery UI (et sa CSS) dans mon fichier "_Layout.cshtml" :

<link href="@Url.Content("~/Content/themes/base/jquery.ui.all.css")" rel="stylesheet" type="text/css" />
<script src="@Url.Content("~/Scripts/jquery-ui-1.8.11.min.js")" type="text/javascript"></script>

Puis je fais évoluer mon EditorTemplate pour ajouter une classe CSS afin que jQuery puisse identifier les zones de saisie de date :

@model DateTime
@Html.TextBox("", String.Format("{0:d}", Model.Date.ToShortDateString()), new { @class = "datefield" })

Et je n'ai plus qu'à utiliser la fonction jQuery ready pour indiquer que tous les éléments qui ont la classe CSS "datefield" doivent être complété d'un calendrier :

<script type="text/javascript">
$(function () {
    $(".datefield").datepicker();
});
</script>

Personnellement, j'ai placé ce script dans mon fichier "_Layout.cshtml" plutôt que dans le template "DateTime.cshtml" pour éviter qu'il soit répété (et donc ré-exécuté) autant de fois qu'il y a de date dans mon formulaire de saisie.

Et maintenant, quand je suis en saisie d'une date, j'ai le calendrier de jQuery UI qui apparait :

jquery-datepicker-mvc-03.jpg

Un calendrier en français

C'est pas mal, mais c'est tout en anglais :) Heureusement, il y a moyen d'avoir une version traduite en français très facilement. Il suffit de récupérer le fichier "jquery.ui.datepicker-fr.js" dans le référentiel Git de jQuery UI : https://github.com/jquery/jquery-ui/blob/master/ui/i18n/ puis de l'enregistrer dans le répertoire "Scripts" de la solution (et de penser à l'inclure dans le projet).

Il ne reste alors plus qu'à référencer ce script (après le script pour jQuery UI ?) dans le layout :

<script src="@Url.Content("~/Scripts/jquery.ui.datepicker-fr.js")" type="text/javascript"></script>

Et cerise sur le gâteau, le calendrier est maintenant en mesure de reconnaitre la date en cours et de s'y positionner correctement :

jquery-datepicker-mvc-04.jpg

Le cas des dates nullables

En creusant un peu sur différents exemples d'utilisation du Datepicker de jQuery UI avec ASP.NET MVC, je suis tombé sur des démos qui allaient un peu plus loin et qui prenaient en compte le cas où la date était nulle.

Dans ce cas là, il faut que les templates "Datetime.cshtml" héritent de l'objet DateTime? et plus de l'objet DateTime. Et donc modifier le code pour gérer le fait qu'on a à faire à un objet nullable, ce qui au final donne les templates suivants :

\Views\Shared\DisplayTemplates\DateTime.cshtml

@model System.DateTime?
@(Model.HasValue ? Model.Value.Date.ToShortDateString() : string.Empty)

\Views\Shared\EditorTemplates\DateTime.cshtml

@model System.DateTime?
@Html.TextBox("", Model.HasValue ? Model.Value.Date.ToShortDateString() : string.Empty, new { @class = "datefield" })

Pour tester que ça marchait, j'ai dû ajouter un seul "?" à ma classe "Livre" :

public class Livre
{
    public int ID { get; set; }
    public string Titre { get; set; }
    public DateTime? Edition { get; set; }
}

Et après ça j'ai dû supprimer ma base de données (le fichier App_Data\Livres.sdf dans mon cas) puis qu'elle avait changé. Chercher "Changing our Model and Database Schema" sur le billet VS 2010 SP1 and SQL CE de Scott Guthrie pour plus d'explications.

Note : j'ai vu des exemples qui initialisent une valeur par défaut lorsque la date est nulle (genre Model.HasValue ? Model.Value.Date.To...() : DateTime.Today.To...()). Mais selon moi, ce n'est pas quelque chose qui doit être décidé et accompli au niveau d'un template. Il est préférable de prévoir ce genre d'initialisation dans une classe ViewModel.

Moderniser le code HTML

Le déclenchement du calendrier est basé sur la présence de la classe CSS "datefield" (ce que fait $(".datefield").datepicker();). Mais c'est quasiment la préhistoire du Javascript non intrusif. Je pense qu'actuellement, il vaut bien mieux se baser sur l'attribut type qui est justement prévu pour définir une saisie de date en HTML5.

Et donc, plutôt que d'ajouter une classe CSS "datefield", le template Editor va directement ajouter un attribut type="date" :

@model System.DateTime?
@Html.TextBox("", Model.HasValue ? Model.Value.Date.ToShortDateString() : string.Empty, new { @type = "date" })

Il faut alors revoir la fonction jQuery ready pour que désormais elle prenne en compte les éléments ayant ce type :

<script type="text/javascript">
$(function () {
    $("input[type=date]").datepicker();
});
</script>

Et on va même plus loin en utilisant la librairie Modernizr promue par ASP.NET MVC pour appliquer le calendrier de jQuery UI uniquement lorsque le navigateur ne prend pas en charge la saisie des dates :

<script type="text/javascript">
$(function () {
    if (!Modernizr.inputtypes.date) {
        $("input[type=date]").datepicker();
    }
});
</script>

Conclusion

Finalement, c'est pas hyper compliqué de faire des templates. Et en y réfléchissant un peu mieux, je pense que c'est une solution qui devrait me plaire parce qu'elle est totalement "discrète" :

  • côté client, le calendrier est appliqué de façon non intrusive : le code de la balise input n'a pas été affublé d'un onclick pour lui attacher un calendrier.
  • côté serveur, le calendrier est généré de façon non intrusive : le formulaire a conservé @Html.EditorFor(model => model.Edition) sans qu'on ait à le défigurer avec un helper spécifique genre @Html.DateTimeFor(model => model.Edition).

vendredi 8 avril 2011

Gérer les virgules avec les Data Annotations

Après avoir "réparé" mon Visual Studio 2010, j'ai pu me remettre à la version 2 du tutoriel MVC Music Store pour me confronter aux dernières (pour moi) technologies ASP.NET MVC 3 et entre autre Razor et les Data Annotations.

Et justement, j'ai été un peu embarrassé par le fonctionnement de la validation via les Data Annotations dès lors qu'on n'est pas des yankees pure souche.

Par exemple, si je veux passer le prix d'un album de 8,99 à 8,90 j'obtiens l'erreur "The field Prix must be a number." de la part du plugin jQuery Validation.

Et si j'essaie de contourner en saisissant 8.90 (avec un point au lieu de la virgule), c'est l'erreur "The value '8.90' is not valid for Prix." qui prend le relai. Mais dans ce cas, cette erreur n'est pas renvoyée par jQuery Validation mais par la méthode TryUpdateModel() dans le contrôleur : mon PC étant en français, le .NET exige une virgule comme séparateur décimal.

Zut ! Déjà les messages en anglais c'est pas tip-top. Mais que ça m'affiche des valeurs numériques avec des virgules et que ça me gueule dessus quand j'essaie de saisir c'est un peu pénible quand même.

Jusqu'à présent, plutôt que de chercher à gérer le problème virgule, je me contentais de bidouiller la section «globalization» dans le fichier web.config pour que ASP.NET prenne lui aussi le "." comme séparateur décimal :

<configuration>
   <system.web>
      <globalization culture="en-US" />
   </system.web>
</configuration>

Mais cette fois-ci, je me suis dit que j'allai creuser un peu plus sinon ça enlève pas mal d'intérêt aux Data Annotations.

Localiser les messages du plugin jQuery Validation

Déjà, quand on fait des recherches sur la localisation de jQuery Validation, on se rend compte que c'est un problème général qui semble avoir été un peu laissé de côté...

Par contre, si on regarde dans le repository du plugin, il existe un répertoire localization qui contient un fichier messages_fr.js avec les messages d'erreur en français.

Super ! Mais ça ne sert pas à grand chose parce que les messages d'erreurs sont déjà initialisés directement par ASP.NET MVC :(

<input data-val="true" 
       data-val-number="The field Prix must be a number."
       data-val-range="Le champ Prix doit &amp;#234;tre compris entre 0,01 et 100."
       data-val-range-max="100" 
       data-val-range-min="0.01" 
       data-val-required="Le champ Prix est requis." 
       id="Price" name="Price" type="text" value="8,99" />

C'est malin ça de définir les messages d'erreur alors que le plugin jQuery Validation les initialise déjà de son côté. Et c'est encore plus rigolo d'en mettre certains en français et d'autres en anglais :)

Localiser "The field Xxxxx must be a number."

Première méthode pour traduire ce message : supprimer l'attribut "data-val-number" de toutes les zones de saisie et inclure le fichier messages_fr.js (attention, l'ordre des scripts est important) :

<script type="text/javascript">
    $(document).ready(function () {
        $(":input[data-val-number]").attr("data-val-number", "");
    });
</script>

<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>

<script type="text/javascript">

    /* ----- messages_fr.js ----- */
    /*
    * Translated default messages for the jQuery validation plugin.
    * Locale: FR
    */
    jQuery.extend(jQuery.validator.messages, {
        required: "Ce champ est requis.",
        remote: "Veuillez remplir ce champ pour continuer.",
        email: "Veuillez entrer une adresse email valide.",
        url: "Veuillez entrer une URL valide.",
        date: "Veuillez entrer une date valide.",
        dateISO: "Veuillez entrer une date valide (ISO).",
        number: "Veuillez entrer un nombre valide.",
...

Après ça, la saisie d'une valeur incorrecte dans la zone prix n'affiche plus l'erreur "The field Prix must be a number." mais "Veuillez entrer un nombre valide.".

Deuxième méthode : si on veut continuer à indiquer le nom du champ qui pose problème dans le message d'erreur, il faut sortir l'artillerie lourde et utiliser les expressions régulières (l'ordre des scripts est toujours important) :

<script type="text/javascript">
    $(document).ready(function () {
        var reg_us = /The field (.+) must be a number\./gi;
        var msg_fr = "Le champ $1 doit être un nombre.";
        $(":input[data-val-number]").each(function () {
            var message = $(this).attr("data-val-number");
            message = message.replace(reg_us, msg_fr);
            $(this).attr("data-val-number", message);
        });
    });
</script>

<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>

<script type="text/javascript">

    /* ----- messages_fr.js ----- */
    /*
    * Translated default messages for the jQuery validation plugin.
    * Locale: FR
    */
    jQuery.extend(jQuery.validator.messages, {
        required: "Ce champ est requis.",
...

Dans ce cas, la saisie d'une valeur incorrecte dans le prix affiche le message "Le champ Prix doit être un nombre.".

Faire accepter les nombres à virgule à jQuery Validation

C'est bien beau de parler à l'utilisateur en français, mais c'est quand même plus important de lui permettre de pouvoir saisir le prix qu'il veut sans avoir à abandonner les valeurs décimales.

Et là, le plugin jQuery Validation a tout prévu puisque le répertoire localisation contient également des fichiers methods_de.js, methods_nl.js et methods_pt.js. Mais malheureusement pour moi, pas de methods_fr.js en vue :(

En y regardant de plus près, le fichier methods_de.js devrait faire l'affaire. Ce qui se confirme sur le forum developpez.com.

Ni une, ni deux, il suffit de compléter les scripts de la façon suivante (l'ordre des scripts est important) :

<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>

<script type="text/javascript">
jQuery.extend(jQuery.validator.methods, {
    date: function(value, element) {
        return this.optional(element) || /^\d\d?\.\d\d?\.\d\d\d?\d?$/.test(value);
    },
    number: function(value, element) {
        return this.optional(element) || /^-?(?:\d+|\d{1,3}(?:\.\d{3})+)(?:,\d+)?$/.test(value);
    }
});
</script>

Et maintenant si je change mon prix en 8,90, je n'ai plus le message "Le champ Prix doit être un nombre.". Non. Maintenant ça me dit que "Le champ Prix doit être compris entre 0,01 et 100.".

C'est des coriaces !

Gérer les virgules dans jQuery Validation

Si c'est ça, je regarde dans le source de jquery.validate.js. Ah ben bien sûr, y'a un problème avec le "range" :

range: function( value, element, param ) {
    return this.optional(element) || ( value >= param[0] && value <= param[1] );
},

Il compare value (le 8,90 que j'ai saisi) avec param[0] (la valeur minimum de 0.01) et param[1] (la valeur maximum de 100). Personne lui a jamais dit qu'on ne peut pas comparer les points et les virgules ?

Et le pire, c'est qu'il fait pareil avec les fonctions "min" et max" le bougre ! Ca va pas se passer comme ça :

/* ----- methods_fr.js ----- */
/*
* Localized default methods for the jQuery validation plugin.
* Locale: FR
*/
jQuery.extend(jQuery.validator.methods, {
    min: function (value, element, param) {
        return this.optional(element) || replaceComma(value) >= replaceComma(param);
    },
    max: function (value, element, param) {
        return this.optional(element) || replaceComma(value) <= replaceComma(param);
    },
    range: function (value, element, param) {
        value = replaceComma(value);
        return this.optional(element) || (value >= replaceComma(param[0]) && value <= replaceComma(param[1]));
    },
    date: function (value, element) {
        return this.optional(element) || /^\d\d?\.\d\d?\.\d\d\d?\d?$/.test(value);
    },
    number: function (value, element) {
        return this.optional(element) || /^-?(?:\d+|\d{1,3}(?:\.\d{3})+)(?:,\d+)?$/.test(value);
    }
});

function replaceComma(value) {
    // Quick & Dirty replace "," by "." as decimal separators
    return value.replace(",", ".");
}

Et après ça, 8,90 passe enfin alors que 100,01 est bien refusé !

Ouf ! Ca devrait faire l'affaire jusqu'à la sortie de jQuery Validation 2.0.

Mise à jour : si j'avais cherché mieux, j'aurais pu trouver le billet Using MVC 3 with non-English Locales de Rick Anderson, le co-auteur du tutoriel Getting Started With MVC3.

mardi 3 novembre 2009

Gestion de contacts avec ASP.NET MVC et jQuery

La dernière étape du tutoriel Développer une application de gestion de contacts avec ASP.NET MVC consistait à ajouter de l'Ajax dans l'application pour la rendre plus performante et plus moderne. Pour parvenir à cela, le tutoriel utilisait Ajax.NET pour les requêtes Ajax et jQuery pour les animations.

De mon côté, j'ai préféré faire entièrement confiance à la librairie jQuery : à la fois pour les animations et pour les fonctionnalités Ajax.

Utilisation d'une vue partielle via jQuery

Il suffit de remplacer les 3 fonctions Javascript beginContactList(), successContactList() et failureContactList() par le tout petit script suivant :

<script type="text/javascript">

    $(document).ready(function() {

        // Ajax Loading
        $("#leftColumn li a").click(function() {

            $("#leftColumn li").removeClass('selected');
            $(this).parent().addClass('selected');

            var url = $(this).attr("href");

            $("#divContactList")
                .fadeOut()
                .load(url, function() {
                    $(this).fadeIn();
                });

            return false;

        });

    });

</script>

Explication de code :

  1. $("#leftColumn li a") pour toutes les balises <a> comprises dans un élément <li> apparaissant dans la balise ayant l'identifiant leftColumn
  2. .click(function() on associe une fonction à l'évènement "click" de ces balises
  3. $("#leftColumn li").removeClass('selected'); supprime la classe "selected" de tous les éléments <li>
  4. $(this).parent().addClass('selected'); ajoute la classe "selected" au parent du lien <a> qui a été cliqué
  5. var url = $(this).attr("href"); retrouve l'url correspondant au lien <a> qui a été cliqué
  6. $("#divContactList") sélectionne l'élément ayant l'identifiant divContactList
  7. .fadeOut() fait disparaitre progressivement l'élément sélectionné
  8. .load(url, function() { charge un contenu externe pointé par url dans l'élément sélectionné puis appelle une fonction lorsque le chargement est terminé
  9. $(this).fadeIn(); fait apparaitre progressivement l'élément sélectionné
  10. return false; annule l’action en cours, c'est à dire le clic sur un lien => le navigateur ne vas pas charger la page dont l'url est définie dans la propriété href de la balise <a>

L'avantage avec cette solution, c'est que du point de vue du développeur, on doit connaitre uniquement jQuery et pas jQuery (pour les animations destinées à rassurer l'utilisateur) + Ajax.NET.

D'autre part, on continue à utiliser la fonction Html.ActionLink() au lieu de la fonction Ajax.ActionLink() au niveau de la vue Index.aspx :

    <ul id="leftColumn">
    <% foreach (var item in Model.Groups) { %>
        <li<%= Html.Selected(item.Id, Model.SelectedGroup.Id) %>>
            <%= Html.ActionLink(item.Name, "Index", new { id = item.Id }) %>
        </li>
    <% } %>
    </ul>

Ce qui en HTML donne le code suivant :

    <ul id="leftColumn">
        <li class="selected">
            <a href="/Contact/Index/1">Business</a>
        </li>
        <li>
            <a href="/Contact/Index/2">Friends</a>
        </li>
    </ul>

On a bien une balise <a> toute simple (<a href="/Contact/Index/1">Business</a>) qui est parfaite pour être sélectionnée en jQuery avec l'expression $("#leftColumn li a").

Pour mémoire, utiliser Ajax.NET et sa méthode Ajax.ActionLink(), c'est pas l'horrible soupe des WebForms, mais ça commence quand même à faire peur :

    <ul id="leftColumn">
        <li class="selected">
                <a groupid="1" href="/Contact/Index/1" onclick="Sys.Mvc.AsyncHyperlink.handleClick(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace, updateTargetId: 'divContactList', onBegin: Function.createDelegate(this, beginContactList), onFailure: Function.createDelegate(this, failureContactList), onSuccess: Function.createDelegate(this, successContactList) });">Business</a>
        </li>
        <li>
                <a groupid="2" href="/Contact/Index/2" onclick="Sys.Mvc.AsyncHyperlink.handleClick(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace, updateTargetId: 'divContactList', onBegin: Function.createDelegate(this, beginContactList), onFailure: Function.createDelegate(this, failureContactList), onSuccess: Function.createDelegate(this, successContactList) });">Friends</a>
        </li>
    </ul>

Faire les suppressions via jQuery

Là aussi, c'est un jeu d'enfant que de se débarrasser de Ajax.NET :

<script type="text/javascript">

    $(document).ready(function() {

        // Ajax Loading
        ...

        // Ajax Deletes
        $(".delete a").click(function() {
            var answer = confirm('Delete contact?');
            if (answer == true) {
                var url = $(this).attr("href");
                $("#divContactList")
                    .fadeOut()
                    .html($.ajax({
                        type: "DELETE",
                        url: url,
                        cache: false,
                        async: false
                    }).responseText)
                    .fadeIn();
            }
            return false;
        });

    });

</script>

On associe une fonction à tous les liens compris dans une classe "delete" (ce qui se dit $(".delete a").click(function() { ... }) en jQuery). Cette fonction commence par demander à l'utilisateur de confirmer qu'il veut bien supprimer le contact (ce qui se dit "Delete Contact?" en anglais). Si l'utilisateur répond par l'affirmative (answer == true), on fait disparaitre la balise contenant la liste des contacts, on remplace son contenu par le résultat d'une requête Ajax puis on la fait ré-apparaitre.

Et pour finir, on fait un return false; pour éviter que le navigateur suive le lien cliqué et atterrisse sur le formulaire de suppression qu'on a laissé là pour les navigateurs qui ne supportent pas le Javascript.

Pour information, j'ai utilisé le paramètre async: false pour attendre que la requête Ajax soit terminée côté serveur. En effet, ce n'est qu'à la fin de l'action AjaxDelete() que le contrôleur nous renvoie la liste des contacts mis à jour. Sans ce paramètre, le navigateur lancerait la requête Ajax puis mettrait immédiatement à jour le contenu de la divContactList, ce qui ne marcherait pas puisque la requête Ajax n'aurait encore rien renvoyé.

Mise à jour (12/11/2009)

Cette méthode présente un tout petit défaut. La fonction jQuery chargée d'ajaxifier les liens pour la suppression s'exécute une fois que la page a été chargée (c'est le propre de la méthode $(document).ready( ... )).

Le problème, c'est que lorsque on change de groupe, on remplace dynamiquement une partie du contenu de la page sans la recharger entièrement ! Et c'est là que le bât blesse. Les liens suppression du nouveau contenu ne sont donc pas ajaxifiés et se comportent de façon classique :

  • lien vers la vue destinée à faire confirmer la suppression du contrat
  • post vers l'action Delete du contrôleur Contact en cas de confirmation

Heureusement, ce n'est pas trop compliqué à corriger. Il suffit de penser à relancer le bout de code jQuery $(".delete a").click( ... ) une fois le contenu mis à jour.

Au final, cela donne donc le source javascript suivant :

<script type="text/javascript">

    $(document).ready(function() {

        // Ajax Loading
        $("#leftColumn li a").click(function() {
            $("#leftColumn li").removeClass('selected');
            $(this).parent().addClass('selected');
            var url = $(this).attr("href");
            $("#divContactList")
                .fadeOut()
                .load(url, function() {
                    $(this).fadeIn();
                    BindDelete();
                });
            return false;
        });

        // Ajax Deletes on page loading
        BindDelete();

    });

    function BindDelete() {
        // Ajax Deletes
        $(".delete a").click(function() {
            var answer = confirm('Delete contact?');
            if (answer == true) {
                var url = $(this).attr("href");
                $("#divContactList")
                    .fadeOut()
                    .html($.ajax({
                        type: "DELETE",
                        url: url,
                        cache: false,
                        async: false
                    }).responseText)
                    .fadeIn();
                    BindDetete();
            }
            return false;
        });
    }    

</script>

Billet suivant dans la série : Portage du tutoriel Contact Manager sous LINQ to SQL

mardi 10 mars 2009

Premier essai pour organiser le plan d'un site

Avant Altrr-Press il y avait QC et avant QC il y avait inPortal. Et à l'époque, il existait un module pour mettre à jour le plan du site.

Ca permettait de déplacer un écran avant ou après un autre écran en cliquant sur les flèches haut et bas et aussi de le changer de niveau en cliquant sur les flèches gauche et droite. Et même si ça devenait un peu compliqué à manipuler dès que le site commençait à avoir un peu trop de pages mais ça rendait bien service.

Pour l'occasion, j'ai ressorti les vieux zip et j'ai reconstitué une page de démonstration, mais sans garantie (ça marchotte sous Firefox parce que c'est tellement vieux qu'en ce temps là j'utilisais encore K-Meleon).

Quand j'ai commencé à travailler sur QC, je me disais toujours qu'il fallait que je refasse une boite pour modifier le plan du site mais soit je n'avais pas envie de faire la même chose qu'avant, soit il y avait d'autres trucs à faire, soit c'était les vacances... Tant et si bien que jusqu'à présent il n'existe toujours rien qui permet de changer l'ordre des pages ou de les ré-organiser dans Altrr-Press.

Actuellement, si l'écran a est avant l'écran b et que l'on veut faire passer l'écran b en première position, il faut :

  • créer un écran c avant l'écran a
  • déplacer dans l'écran c toutes les boites de l'écran b
  • supprimer l'écran b
  • renommer l'écran c en ecran b

L'avantage avec cette méthode c'est que cela oblige à réfléchir 5 minutes au plan du site avant de se mettre à créer les pages.

Mais là, avec tout les trucs qui existent sous jQuery, je me dit qu'il serait quand même temps d'essayer d'offrir une méthode un peu plus pratique pour réorganiser la structure des sites créés avec Altrr-Press.

J'ai donc fait un premier essai avec le composant Sortable de jQuery UI. Pouvoir organiser le plan du site à coup de drag & drop, ça devrait le faire. L'avantage, c'est qu'une fois qu'on a trouvé un exemple qui marche, c'est assez simple à utiliser.

Il faut commencer par insérer tous les fichiers javascript nécessaires :

<script type="text/javascript" src="jquery-1.3.2.min.js"></script>
<script type="text/javascript" src="ui.core.js"></script>
<script type="text/javascript" src="ui.sortable.js"></script>

Puis on appelle la méthode sortable() pour le 1° niveau de la liste que l'on veut pouvoir trier :

<script type="text/javascript">
$(document).ready(function() {
  var sortOpts = {
    items: "li", 
    cursor: "crosshair"
  }
  $("ul.ap-sitemap").sortable(sortOpts);
});
</script>

C'est déjà pas mal, mais :

  • ça tremblote un peu quand on est en mode drag
  • on a du mal à prévoir où on va dropper

Si je rajoute un peu de CSS et que je met en évidence la zone où le drop va avoir lieu :

C'est déjà plus clair, mais je reste un peu sur ma faim :

  • c'est mieux que rien mais sans plus... je m'attendais à un effet plus spectaculaire
  • je n'arrive pas à créer un sous-niveau : pendant un moment j'ai pensé que la propriété dropOnEmpty me le permettrait mais apparemment ça n'est pas ça.
  • et en plus, il faudra que je cherche comment limiter le nombre de sous-niveau à 3

A creuser donc...

Note : la liste que j'utilise en exemple est tirée de l'article Complex Dynamic Lists: Your Order Please de Christian Heilmann et publié sur A List Apart.