Drag and drop en html et javascript

Bonjour,

Aujourd’hui j’ai du implémenter un outil pour réorganiser une arborescence.

Il y a donc une imbrication de <ul> (et les <li> qui vont avec) et l’utilisateur pourra bouger n’importe quelle partie de l’arborescence.

Mise en place basique

Voici un exemple d’arborescence en html :

<!DOCTYPE html>
<html>
    <head>
        <title>Drag and drop</title>
    </head>
    <body>
        <ul>
            <li>Element 1</li>
            <li>Element 2</li>
            <li>Element 3
                <ul>
                    <li>Element 4</li>
                </ul>
            </li>
        </ul>
    </body>
</html>

Il faut rendre les <li> draggable avec l’attribut draggable= »true »

Ensuite faire une balise <script>, de préférence en fin de page, avant </body>.

On y mettra les fonctions pour le drag and drop (drag pour ondragstart et drop pour ondrop)

function drag(ev) {
}
function drop(ev) {
    ev.preventDefault();
}
function dragOver(ev) {
    ev.preventDefault();
}

Ceci ne suffit pas, il faut affecter ces fonctions aux évènements des éléments concernés. Mais on va faire ça bien, c’est à dire dans une fonction init qui se lancera dès que le dom est prêt.

function init() {
    // attache drop
    elements = document.querySelectorAll('li');
    for (i = 0 ; i < elements.length ; i++) {
        elements[i].addEventListener('drop', drop);
        elements[i].addEventListener('dragover', dragOver);
    }
    // attache drag
    elements = document.querySelectorAll('li[draggable]');
    for (i = 0 ; i < elements.length ; i++) {
        elements[i].addEventListener('dragstart', drag);
    }
}
document.addEventListener('DOMContentLoaded', init);

J’attache drop avant drag pour du chipotage, je donnerais la réponse en commentaire si ça intéresse quelqu’un. En plus, j’attache drag seulement aux éléments draggable et drop à tous les li, juste pour montrer que c’est pas forcément en double sens.

On y est presque, il faut maintenant détacher l’élément source et l’attacher dans l’élément cible. Pour cela, il faut générer un id dans les éléments draggable si ils en ont pas déjà un.

// attache drag
elements = document.querySelectorAll('li[draggable]');
for (i = 0 ; i < elements.length ; i++) {
    id = elements[i].getAttribute('id');
    if (id == null || id == '') {
        elements[i].setAttribute('id', 'draggable'+i);
    }
    elements[i].addEventListener('dragstart', drag);
}

Maintenant que tout est en place, on peut donner les actions de déplacement dans le dom.

function drag(ev) {
    element = ev.target;
    ev.dataTransfer.setData('text', element.getAttribute('id'));
}
function drop(ev) {
    ev.preventDefault();
    data = ev.dataTransfer.getData('text');
    element = document.getElementById(data);
}

Ca marche mais une fois ou deux, ce code, il y a plein de bogue !

Amélioration et hierarchie

On va voir ce qui se passe et corriger. On met directement un <li> dans un autre <li>, il faudrait construire un <ul> et mettre notre <li> dedans, comme ça on aura corrigé le problème et on peut avoir une hiérarchie.

function drop(ev) {
    ev.preventDefault();
    data = ev.dataTransfer.getData('text');
    element = document.createElement('ul');
        element2 = document.getElementById(data);
        parent = element2.parentElement;
        element.appendChild(element2);
    ev.target.appendChild(element);
}

Ca marche bien maintenant. Mais quand je lâche un élément sur lui même, il disparaît.

Empêcher la source d’être la cible

Ce petit problème est en fait la raison d’être de cet article.

Je vais d’abord exposer la méthode qui m’est naturellement venue à l’esprit :

Dans drag, on annule les évènements drop pour l’élément choisi. Puis dans drop, on remet les évènements pour tout le monde.

function drag(ev) {
    element = ev.target;
    ev.dataTransfer.setData('text', element.getAttribute('id'));
    // annule drop pour la cible et ses enfants
    element = ev.target;
    elements = element.querySelectorAll('li');
    element.removeEventListener('drop', drop);
    element.removeEventListener('dragover', dragover);
    for (i = 0 ; i < elements.length ; i++) {
        elements[i].removeEventListener('drop', drop);
        elements[i].removeEventListener('dragover', dragover);
    }
}
function drop(ev) {
    ev.preventDefault();
    data = ev.dataTransfer.getData('text');
    element = document.createElement('ul');
    element2 = document.getElementById(data);
    // remet drop pour la cible et ses enfants
    elements = element2.querySelectorAll('li');
    element2.addEventListener('drop', drop);
    element2.addEventListener('dragover', dragover);
    for (i = 0 ; i < elements.length ; i++) {
        elements[i].addEventListener('drop', drop);
        elements[i].addEventListener('dragover', dragover);
    }
    parent = element2.parentElement;
    element.appendChild(element2);
    ev.target.appendChild(element);
}

Ca marche quand on se trompe pas, mais des fois on drop là ou ça ne lance pas drop.

Il faut donc tout faire dans drag. Dès qu’on drag (ouais, je vais me passer de faire une blague ici), on reset les drop (on met les drop à tout le monde) et on empêche l’élément et ses descendants de drop.

function drag(ev) {
    element = ev.target;
    ev.dataTransfer.setData('text', element.getAttribute('id'));
    // remet drop pour tout le monde
    elements = document.querySelectorAll('li');
    for (i = 0 ; i < elements.length ; i++) {
        elements[i].addEventListener('drop', drop);
        elements[i].addEventListener('dragover', dragover);
    }
    // annule drop pour la cible et ses enfants
    element = ev.target;
    elements = element.querySelectorAll('li');
    element.removeEventListener('drop', drop);
    element.removeEventListener('dragover', dragover);
    for (i = 0 ; i < elements.length ; i++) {
        elements[i].removeEventListener('drop', drop);
        elements[i].removeEventListener('dragover', dragover);
    }
}
function drop(ev) {
    ev.preventDefault();
    data = ev.dataTransfer.getData('text');
    element = document.createElement('ul');
    element2 = document.getElementById(data);
    parent = element2.parentElement;
    element.appendChild(element2);
    ev.target.appendChild(element);
}

C’est déjà mieux, mais quand on joue trop longtemps, il y a des fois où on peut faire un drag and drop sur l’élément ou son descendant, et ça fait planter.

J’ai essayé de me dépatouiller avec les events, mais en vain. J’ai donc décidé de mettre un drapeau sur les éléments qui ne doivent pas accepter le drop.

function drag(ev) {
    element = ev.target;
    ev.dataTransfer.setData('text', element.getAttribute('id'));
    // remet drop pour tout le monde
    elements = document.querySelectorAll('li');
    for (i = 0 ; i < elements.length ; i++) {
        elements[i].removeAttribute('data-notarget');
    }
    // annule drop pour la cible et ses enfants
    element = ev.target;
    elements = element.querySelectorAll('li');
    element.setAttribute('data-notarget', 'true');
    for (i = 0 ; i < elements.length ; i++) {
        elements[i].setAttribute('data-notarget', 'true');
    }
}
function drop(ev) {
    isTarget = ev.target.getAttribute('data-notarget');
    if (isTarget == null || isTarget == '') {
        ev.preventDefault();
        data = ev.dataTransfer.getData('text');
        element = document.createElement('ul');
        element2 = document.getElementById(data);
        parent = element2.parentElement;
        element.appendChild(element2);
        ev.target.appendChild(element);
    }
}
function dragover(ev) {
    isTarget = ev.target.getAttribute('data-notarget');
    if (isTarget == null || isTarget == '') {
        ev.preventDefault();
    }
}

On a une solution plus robuste, mais il y a quand même quelque plantages.

Après une multitude de tests, j’ai réussit à trouver la cause : l’élément <ul> est aussi droppable et execute drop quand on vise bien dessus (sur la puce par exemple).

Je considère que c’est un bug et pas une feature, car si j’aurais voulu que <ul> soit droppable, je lui aurais affecté manuellement cet évènement.

Donc, il faut inhiber le drop sur <ul>

function drop(ev) {
    isTarget = ev.target.getAttribute('data-notarget');
    if (isTarget == null || isTarget == '') {
        ev.preventDefault();
        data = ev.dataTransfer.getData('text');
        element = document.createElement('ul');
        element.setAttribute('data-notarget', 'true');
        element2 = document.getElementById(data);
        parent = element2.parentElement;
        element.appendChild(element2);
        ev.target.appendChild(element);
    }
}
function init() {
    // exclusion de ul
    elements = document.querySelectorAll('ul');
    for (i = 0 ; i < elements.length ; i++) {
        elements[i].setAttribute('data-notarget', 'true');
    }
    // attache drop
    elements = document.querySelectorAll('li');
    for (i = 0 ; i < elements.length ; i++) {
        elements[i].addEventListener('drop', drop);
        elements[i].addEventListener('dragover', dragover);
    }
    // attache drag
    elements = document.querySelectorAll('li[draggable]');
    for (i = 0 ; i < elements.length ; i++) {
        id = elements[i].getAttribute('id');
        if (id == null || id == '') {
            elements[i].setAttribute('id', 'draggable'+i);
        }
        elements[i].addEventListener('dragstart', drag);
    }
}

Maintenant, c’est du code robuste, qui ne plante plus. Mais si je change <ul> par <ol> de partout, j’ai que des 1 qui se suivent. Pourquoi ?

C’est parcequ’on ne se pose pas la question de savoir si il existe un <ul> (ou <ol> quand on a fait le changement). On fabrique <ul>, on remplit, puis on ajoute au dom. On ne vérifie pas non plus si le <ul> qu’on laisse derrière est vide. Il faut donc nettoyer tout ça.

Nettoyage du dom et amélioration de la hiérarchie

En plus du nettoyage, j’ai un petit souci avec la hierarchie. Les éléments ne peuvent pas passer en premier ordre. La solution que j’ai trouvé, c’est de faire un <ul> qui permettra de déposer n’importe quel élément, mais qui n’est pas draggable.

Pour le nettoyage du dom, tout est dit un peu plus haut.

<ol>
    <li>D&eacute;poser ici les &eacute;l&eacute;ments de plus haut niveau
        <ol>
            <li draggable="true">Element 1</li>
            <li draggable="true">Element 2</li>
            <li draggable="true">Element 3
                <ol>
                    <li draggable="true">Element 4</li>
                </ol>
            </li>
        </ol>
    </li>
</ol>
function drop(ev) {
    isTarget = ev.target.getAttribute('data-notarget');
    if (isTarget == null || isTarget == '') {
        ev.preventDefault();
        data = ev.dataTransfer.getData('text');
        elements = ev.target.getElementsByTagName('ol');
        element = null;
        if (elements.length != 0) {
            element = elements[0];
        } else {
            element = document.createElement('ol');
            element.setAttribute('data-notarget', 'true');
        }
        element2 = document.getElementById(data);
        parent = element2.parentElement;
        element.appendChild(element2);
        ev.target.appendChild(element);
        if (parent.children.length == 0) {
            parent.remove();
        }
    }
}

Voilà, on a un drag and drop qui est fonctionnel et qui est propre.

Peaufinage

Comment on peut faire un effet d’inversion vidéo sur les éléments droppables ?

C’est facile, il suffit de savoir qu’il y a un évènement supplémentaire qui est dragleave (et pas dragout ça aurait été trop simple).

Je ne vais pas donner d’exemple qui fonctionne de partout car il faut gérer les bug de firefox sur dragleave qui donne pas le <li> mais le texte dedans alors que dragover marche bien !

<!DOCTYPE html>
<html>
 <head>
 <title>Drag and drop</title>
 </head>
 <body>
 <ol>
 <li>D&eacute;poser ici les &eacute;l&eacute;ments de plus haut niveau
 <ol>
 <li draggable="true">Element 1</li>
 <li draggable="true">Element 2</li>
 <li draggable="true">Element 3
 <ol>
 <li draggable="true">Element 4</li>
 </ol>
 </li>
 </ol>
 </li>
 </ol>
 <script>
 function drag(ev) {
 element = ev.target;
 ev.dataTransfer.setData('text', element.getAttribute('id'));
 // remet drop pour tout le monde
 elements = document.querySelectorAll('li');
 for (i = 0 ; i < elements.length ; i++) {
 elements[i].removeAttribute('data-notarget');
 }
 // annule drop pour la cible et ses enfants
 element = ev.target;
 elements = element.querySelectorAll('li');
 element.setAttribute('data-notarget', 'true');
 for (i = 0 ; i < elements.length ; i++) {
 elements[i].setAttribute('data-notarget', 'true');
 }
 ev.stopPropagation();
 }
 function drop(ev) {
 target = ev.target;
 isTarget = target.getAttribute('data-notarget');
 if (isTarget == null || isTarget == '') {
 ev.preventDefault();
 ev.target.style.backgroundColor = '#fff';
 data = ev.dataTransfer.getData('text');
 elements = ev.target.getElementsByTagName('ol');
 element.setAttribute('data-notarget', 'true');
 element = null;
 if (elements.length != 0) {
 element = elements[0];
 } else {
 element = document.createElement('ol');
 }
 element2 = document.getElementById(data);
 parent = element2.parentElement;
 element.appendChild(element2);
 ev.target.appendChild(element);
 if (parent.children.length == 0) {
 parent.remove();
 }
 }
 ev.stopPropagation();
 }
 function dragover(ev) {
 target = ev.target;
 isTarget = target.getAttribute('data-notarget');
 if (isTarget == null || isTarget == '') {
 ev.preventDefault();
 ev.target.style.backgroundColor = '#fcc';
 }
 ev.stopPropagation();
 }
 function dragleave(ev) {
 target = ev.target;
 isTarget = target.getAttribute('data-notarget');
 if (isTarget == null || isTarget == '') {
 ev.preventDefault();
 ev.target.style.backgroundColor = '#fff';
 }
 ev.stopPropagation();
 }
 function init() {
 // exclusion de ol
 elements = document.querySelectorAll('ol');
 for (i = 0 ; i < elements.length ; i++) {
 elements[i].setAttribute('data-notarget', 'true');
 }
 // drop related events
 elements = document.querySelectorAll('li');
 for (i = 0 ; i < elements.length ; i++) {
 elements[i].addEventListener('drop', drop);
 elements[i].addEventListener('dragover', dragover);
 elements[i].addEventListener('dragleave', dragleave);
 }
 // drag related events
 elements = document.querySelectorAll('li[draggable]');
 for (i = 0 ; i < elements.length ; i++) {
 id = elements[i].getAttribute('id');
 if (id == null || id == '') {
 elements[i].setAttribute('id', 'draggable'+i);
 }
 elements[i].addEventListener('dragstart', drag);
 }
 }
 document.addEventListener('DOMContentLoaded', init);
 </script>
 </body>
</html>

Ce drag and drop marche bien sur chrome, mais pas sur firefox 45.1.1 a cause de dragleave. Vu que c’est clairement un bug, je ne vais pas investiguer.

WP fastest cache, le verdict

Bonjour,

WordPress n’est pas un foudre de guerre, tout le monde le sait. Comment remédier à sa lenteur ?

Ma solution est d’installer une extension qui gérera un cache.

J’ai déjà testé 2 d’entre elles : W3 total cache et WP fastest cache.

J’ai été déçu par W3 total cache et c’est pour ça que je l’ai remplacé par WP fastest cache.

Après quelques jours d’utilisation, je dirais que l’accélération est aléatoire.

Des fois c’est rapide et d’autres fois c’est lent. Quand c’est lent, j’ai pas l’impression que ce soit plus lent que wordpress sans cache. Donc, rien de perceptible dans le pire des cas.

Je vais donc garder ce gestionnaire de cache pour mon blog.

Scheduled post trigger, le verdict

Bonjour,

Il y a quelques temps, j’ai installé scheduled post trigger pour palier à un bug de wordpress.

Aujourd’hui, je vais donner mon verdict sur l’utilisation de cette extension.

Je vais directement parler du résultat : le travail est fait, mais je ne sais pas quand.

En effet, des fois, je regarde le blog bien après l’heure prévue et l’article n’est pas publié. Puis en revenant dessus, c’est en place. Mais vu que je me connecte sur le back-end en parallèle, je ne sais pas ce qui lance le test des articles en retard sur leur publication.

Je pourrais faire des tests plus poussés pour savoir comment ça marche. Peut-être faire mieux : regarder le code source pour être sûr de ce qui est fait. Ce n’est pas ma priorité pour le moment, je vais donc mettre un point final à cette histoire d’articles en retard.

Comme d’habitude, j’ai une petite victoire ici et je vais m’en tenir à ceci.

nginx et apache

Bonjour,

J’ai du faire une installation qui est de plus en plus répendue sur les serveurs.

Installer nginx et apache sur le même serveur. Apache est en back-end et nginx comme serveur front.

Je vais d’abord donner l’avis de ceux qui mettent en place cette architecture :

Nginx est un serveur rapide, donc on l’utilise. Ensuite, on s’apperçoit que tout ne marche pas (attention, j’ai pas dit que rien ne marchait !). Parmis ce qui ne marche pas, il y a php et l’url rewriting. Pour php, on peut le mettre en fast cgi et le tour est joué. Même si il y a le mot « fast », c’est quand même plus lent qu’un module, comme apache sait le faire. Les experts nginx ne courent pas les rues, donc transcrire le .htaccess n’est pas toujours envisageable (ou suffisament bien fait).

Avec tous ces problèmes, on est tenté de réinstaller apache et de ne plus se casser la tête, mais c’est sans compter sur la hiérarchie qui veut nginx car c’est plus rapide !!!

Il faut donc utiliser nginx pour le contenu statique (pages html, images, javascript, css, etc.) et quand il y a des pages php, il fait passer ça à apache. C’est le reverse proxying.

Et pour le rewriting ?

Là aussi, on voit la grande utilité d’nginx, on sert les fichiers qui existent, sinon on donne la requête à apache.

On a théoriquement le meilleur des 2 mondes. Nginx donne rapidement les pages qui ne demande pas de traitement et apache prête main forte quand il y a du contenu dynamique.

Alors, oui, il y a des pointes de sarcasme dans ce billet.

Pourquoi ?

Et bien j’ai déjà émis mon point de vue sur l’utilisation du cache et que c’est un troc, de l’espace mémoire contre du temps de calcul.

Ici on installe 2 fois le même service (on utilise plus de place en mémoire, c’est un bon départ pour le troc vu plus haut). Il faut régler les téléscopages, donc faire écouter apache sur un autre port (utilisation de resource en plus). Il faut bloquer le port depuis l’extérieur (un peu plus de travail pour le firewall). Nginx doit être réglé pour donner les fichiers directement et faire suivre tout ce qu’il ne comprend pas (on perd donc du temps à rediriger tout le contenu dynamique).

On gagne beaucoup sur le statique et on perd un peu sur le dynamique, mais au final, c’est mieux non ? Comme à mon habitude, la réponse est non !

Il y a dans apache un module mod_expires, il permet de gérer le cache client. Et quand on le règle bien, on ne télécharge plus rien.

Alors c’est quoi qui est mieux ? Nginx qui donne du contenu très rapidement ou apache qui donne un contenu beaucoup plus lentement mais une seule fois dans l’année !

Mon opinion en clair : il faut en prendre 1 et bien le régler. Nginx seul peut gérer les pages statiques, les pages dynamiques, le rewriting et la gestion du cache client. Il faut juste s’investir un peu. Apache peut en faire autant. La combinaison des 2 marche bien pour ceux qui ne font pas d’effort ni de mesure.

Si vous voulez des détails techniques su la mise en place de l’une des configurations, demandez le en commentaire.

Gérer la mémoire cache pour les images

Bonjour,

Je vous ai fait part d’un travail où je devais faire une api de gestion d’images. Voici donc une suite qui me permettra de parler d’une partie de l’implémentation.

C’est la gestion de la mise en cache des images.

Vous savez que je ne suis pas fan des caches, mais quand il y a beaucoup de calculs, il peut y avoir un avantage à utiliser un cache.

Ils sont où les calculs dans cette api ?

Et bien, il y en a beaucoup. Toutes les fonctions, sauf /get, tansforment les images. Ceci implique d’interpoler ou extrapoler les pixels d’une image d’origine vers une image de destination. Le pré-calcule n’est pas de mise. C’est un cache qui ne calcule une image que lorsqu’on en a besoin. Il la stock et renvoie l’image calculée à chaque nouvelle requête.

Comme tout cache, il faut le vider, et ceci arrive quand un client change son image ou lorsqu’il est supprimé.

Ce qui limite aussi la taille du cache, c’est que l’on ne peut pas demander toutes les tailles pour toutes les images, mais uniquement 3 tailles standards.