Un plugin jQuery pour des images en lazy-load

Voici un petit nouveau : un , le bien nommé Lazy ! Passez-lui un sélecteur afin de charger puis d'afficher en fondu les éléments img ciblés au fil du scroll (ou directement pour ceux déjà présents dans le viewport). Pour un rendu optimal, pensez à fixer leur taille dans le code HTML ou CSS !

Découvrez-en plus - et comme toujours, contribuez si vous le voulez ! - sur la page GitHub du projet.

Utiliser le même langage de templating en frontend et en backend

Si vous utilisez un framework JavaScript frontend tel AngularJS, peut-être avez-vous déjà souhaité pouvoir gérer les templates partiels utilisés par celui-ci de manière uniforme avec vos layouts (qui, à n'en pas douter, seront quant à eux générés en backend). Une astuce simple et efficace consiste à rendre ceux-ci disponibles via une route de votre application backend, dont l'unique rôle sera d'en effectuer le rendu afin que le frontend puisse prendre le relais. Au final, le résultat sera le même que la solution basique consistant à entreposer des fichiers HTML dans un dossier public de votre application, la flexibilité et l'uniformité en plus !

Voyons donc un exemple rapide avec la stack MEAN. Côté Express :

app.get('/partials/:name.html', function (req, res) {
res.render(path.join('partials', req.params.name));
});

Le chemin sera bien évidemment à adapter selon votre cas : je suppose ici que les templates concernés se trouvent dans un dossier partials, lui-même enfant direct de votre répertoire de vues principal.

Pour récupérer le tout sur Angular, il suffira donc tout simplement de faire quelque chose de ce genre :

var app = angular.module('myApp', ['ngRoute']);

app.config(['$routeProvider', function ($locationProvider, $httpProvider, $routeProvider)
{
$routeProvider
.when('/some-route', {
templateUrl: '/partials/some-template.html',
controller: 'SomeCtrl'
})

.when('/some-other-route', {
templateUrl: '/partials/some-other-template.html',
controller: 'SomeOtherCtrl'
});
}]);

Ceci vous donne l'avantage non négligeable de pouvoir traiter certains aspects de vos partials depuis le backend, si vous en avez le besoin.

J'oubliais : bonne année à tous !

PrestaShop : de l'usage du champ Position d'un modèle dans le contrôleur de back-office correspondant

Si vous développez régulièrement sur PrestaShop, il y a fort à parier que vous avez déjà écrit vous-même des classes héritant d'AdminController afin d'offrir à vos utilisateurs une interface en back-office leur permettant de gérer les fonctionnalités que vous mettez en place.

Dans le cas d'un module par exemple, vous avez peut-être suivi cet excellent tutoriel de manière à permettre l'administration des entités gérées par ledit module.

Si vous avez ensuite jugé qu'il serait utile d'ajouter aux entités en question un champ position, dont la valeur serait prise en compte lors de l'affichage en front-office (et se présentant en back-office de manière similaire à ce que l'on peut trouver sur les catégories par exemple), je peux cette fois affirmer sans trop risquer de me tromper que vous n'êtes pas près d'oublier la torture mentale qui a suivi. En effet, cet aspect a beau être totalement (ou presque) géré par PrestaShop nativement, l'absence totale de documentation à ce sujet peut s'avérer pour le moins handicapante. Je vous propose donc un petit récapitulatif !

Premièrement, votre modèle doit bien évidemment inclure le champ correspondant :

class MyModel extends ObjectModel
{
public $id_model; // clé primaire
public $position;
// ...

public static $definition = array(
// ...

'fields' => array(
// ...
'position' => array(
'type' => ObjectModel::TYPE_INT
),
// ...
)
);

J'affirmais tantôt que PrestaShop sait gérer "nativement" l'implémentation d'un champ position sur un modèle : ce n'est pas tout à fait vrai, étant donné que les parties du code source dédiées à cet aspect font référence en dur à des champs liés aux catégories (de produit ou de pages CMS), qui sont les seules à en faire usage par défaut. Nous allons donc devoir mettre un peu les mains dans le cambouis pour combler les trous et répliquer ce comportement pour nos propres entités.

Continuons donc sur notre modèle, et créons tout d'abord une méthode pour récupérer facilement la prochaine valeur de position disponible :

public static function getNextAvailablePosition()
{
$sql = 'SELECT position FROM '._DB_PREFIX_.self::$definition['table'].' ORDER BY position DESC';

$position = (int)Db::getInstance()->getValue($sql, false);
return $position + 1;
}

Nous allons ensuite nous assurer que ce champ est correctement renseigné lors de l'enregistrement en base de données d'une nouvelle instance de notre modèle, en étendant la méthode processAdd du contrôleur d'admin de notre module :

public function processAdd()
{
$object = parent::processAdd();
$object->position = MyModel::getNextAvailablePosition();

$object->id_model = $object->id; // apparemment nécessaire si votre clé primaire porte un nom autre qu'"id"
$object->update();

return $object;
}

Si vous testez ce que nous venons d'ajouter maintenant, tout devrait fonctionner correctement en apparence (du moins, en ce qui concerne les valeurs sauvegardées). Voyons maintenant comment "câbler" cette fonctionnalité sur l'interface d'administration.

Ajoutons tout d'abord notre champ position à la vue en liste de nos entités, dans notre classe fille de ModuleAdminController :

public function __construct()
{
// ...
$this->orderBy = 'position'; // on définit le critère de tri par défaut
$this->position_identifier = 'id_model'; // on rappelle la clé primaire du modèle (sans quoi les liens générés seront incorrects)

parent::__construct();

$this->fields_list = array(
// ...
'position' => array(
'title' => $this->l('Position'),
'position' => 'position' // toute la "magie" est là...
),
// ...
);

La nouvelle colonne que vous verrez apparaître contiendra soit des liens contenant des icônes en forme de flèche (le résultat souhaité !), soit des chiffres. Dans ce second cas, assurez-vous que votre liste est actuellement triée selon notre nouveau champ position dans le sens ascendant, ainsi que nous l'avons défini dans le code (en pratique, c'est le dernier critère sélectionné par l'utilisateur qui prévaut).

Une fois les liens permettant de modifier la position d'un élément visibles sur votre vue en liste, nul doute que vous allez sentir l'espoir renaître et les ténèbres se dissiper. Et pourtant, si vous cliquez sur l'un d'eux, vous aurez droit à un résultat des plus somptueux :

Fatal error: Call to undefined method MyModel::updatePosition() in [...]

En effet, la classe de base ObjectModel ne définit pas cette fonctionnalité : le soin est laissé aux classes filles de l'implémenter selon leurs propres besoins le cas échéant. En ce qui nous concerne, nous pouvons y procéder rapidement ainsi :

/**
* @param $way Sens du changement de position (0 si l'élément monte, 1 s'il descend)
* @param $position La position qu'aura l'élément _après_ ledit mouvement
*
* @return Booléen indiquant la réussite ou l'échec de l'opération
*/
public function updatePosition($way, $new_position)
{
$db = Db::getInstance();
$count = $db->getValue('SELECT COUNT(*) FROM '._DB_PREFIX_.self::$definition['table'], false);

if (($new_position >= 1) && ($new_position <= $count)) {
$old_position = $way ? ($new_position - 1) : ($new_position + 1);

if (($old_position >= 1) && ($old_position <= $count)) {
$sql = implode(
';',
array(
'UPDATE '._DB_PREFIX_.self::$definition['table'].' SET position = 0 WHERE position = '.(int)$new_position,
'UPDATE '._DB_PREFIX_.self::$definition['table'].' SET position = '.(int)$new_position.' WHERE position = '.(int)$old_position,
'UPDATE '._DB_PREFIX_.self::$definition['table'].' SET position = '.(int)$old_position.' WHERE position = 0'
)
);

// L'ancienne et la nouvelle position sont valides, on les intervertit
return $db->execute($sql);
}
}

return false;
}

Voilà qui est mieux ! N'oublions pas non plus de réassigner proprement la position des éléments en cas de suppression de l'un d'eux. À cette occasion, nous revenons sur le modèle et lui ajoutons la méthode suivante :

public function delete()
{
$position = $this->position;

if ($result = parent::delete()) {
Db::getInstance()->execute('UPDATE '._DB_PREFIX_.self::$definition['table'].' SET position = position-1 WHERE position > '.(int)$position);
}

return $result;
}

Enfin, en bonus, nous allons faire en sorte que le changement de position par drag'n'drop dans la colonne correspondante ait un réel effet (pour l'instant, la modification se produit visuellement mais n'est pas persistée) en ajoutant la méthode suivante à notre contrôleur :

public function ajaxProcessUpdatePositions()
{
if (isset($_POST['id']) && isset($_POST['way'])) {
$object = new MyModel($_POST['id']);

if ($object->id_model) {
$new_position = $object->position;
$_POST['way'] ? $new_position++ : $new_position--;

if ($object->updatePosition($_POST['way'], $new_position)) {
die;
}

die('{"hasError": true, errors: "Cannot update position"}');
}
}

die('{"hasError": true, errors: "This item can not be loaded"}');
}

Le nom de cette méthode obéit à une convention (secrète ?) de PrestaShop qui lui permet d'être exécutée automatiquement lorsque la requête AJAX idoine est lancée.

Ouf, nous y voilà parvenus ! J'ai écrit cet article à la suite de mes propres tâtonnements, aussi n'hésitez pas à le commenter si certains détails sont inexacts ou imprécis et que, fort de votre propre galère expérience, vous pouvez les améliorer.

Un plugin jQuery pour redimensionner automatiquement vos textarea

Je vais aujourd'hui vous parler de Crusher, un petit destiné à faire en sorte que vos textarea se redimensionnent automatiquement. Léger (il repose en bonne partie sur l'application de styles CSS) et facile d'utilisation, il s'agit (à mon sens !) d'une bonne solution pour réaliser cet effet simplement.

Comme d'habitude, n'hésitez pas à participer au projet en vous rendant sur sa page GitHub !

Gérer des routes particulières sur PrestaShop

PrestaShop permet à ses utilisateurs disposant d'un serveur web supportant l'URL rewriting de gérer les URLs de leur boutique d'une manière assez complète : il suffit de modifier, directement dans le back-office, les masques correspondant à tel ou tel type d'URL pour voir toute la structure de son site se mettre à jour automatiquement, et tout cela est bien beau.

Mais qu'advient-il le jour où, pris de folie, vous vous rendez compte que vous avez besoin de gérer une URL spécifique pour une page ? Prenons l'exemple d'une catégorie : d'un point de vue technique, PrestaShop vous oblige à faire figurer l'id de ladite catégorie dans son URL. Comment faire pour s'en débarrasser sans casser tout le fonctionnement du routeur interne ?

La solution que je vous propose aujourd'hui consiste en une simple surcharge de la classe Dispatcher :

<?php

class Dispatcher extends DispatcherCore
{
// On définit des routes particulières, qui auront la priorité sur le processus de routage basique
// À gauche, l'URL actuelle ; à droite, l'URL souhaitée

public $special_routes = array(
'/3-my-category' => '/my-awesome-url'
);



public function dispatch()
{
// On démarre une session PHP classique pour la persistence des données POST,
// étant donné que PrestaShop lui-même n'en fait pas usage
session_start();

$this->getController();
$request_uri = explode('?', $this->request_uri);

// Si on a sauvegardé des données POST avant redirection, on les récupère
if (isset($_SESSION['_POST'])) {
$_POST = $_SESSION['_POST'];
unset($_SESSION['_POST']);
}

foreach ($this->special_routes as $old_url => $new_url) {
if ($request_uri[0] == $new_url) {
// Une "nouvelle" URL est demandée :
// on passe la requête à sa contrepartie originelle

$_GET['controller'] = false;
$this->controller = false;
$request_uri[0] = $old_url;
$this->request_uri = implode('?', $request_uri);
break;
} else if ($request_uri[0] == $old_url) {
// Une "ancienne" URL est demandée :
// on sauvegarde les données POST s'il y en a,
// et on redirige vers l'URL voulue

$_SESSION['_POST'] = $_POST;
$request_uri[0] = $new_url;
header('Status: 301 Moved Permanently', false, 301);
header('Location: '.implode('?', $request_uri));
die;
}
}

parent::dispatch();
}



// On fait maintenant en sorte que le générateur d'URLs utilise les nôtres
// Cela nous permet de gagner du temps et de nous assurer de la réussite des requêtes AJAX

public function createUrl($route_id, $id_lang = null, array $params = array(), $force_routes = false, $anchor = '', $id_shop = null) {
$url = '/'.parent::createUrl($route_id, $id_lang, $params, $force_routes, $anchor, $id_shop);
return ltrim((isset($this->special_routes[$url]) ? $this->special_routes[$url] : $url), '/');
}
}

Ainsi, dans cet exemple, naviguer vers /my-awesome-url fera apparaître la catégorie voulue ; et si on se rend directement à cette dernière via /3-my-category, on sera redirigé. Les éventuels paramètres GET et POST seront conservés.

Pour que ceci fonctionne, vous devez désactiver l'option de redirection canonique native dans le back-office de PrestaShop (dans Préférences > SEO & URLs). Soyez sûr(e) d'avoir conscience des conséquences !