Simple Slides

SimpleSlides est un projet tout simple, un POC pour illuster la manière dont on peut réaliser rapidement une visionneuses d'images avec du HTML, un peu de CSS et de javascript. Sa seule ambition est d'éventuellement servir de base à des projets plus aboutits et complets.

Présentation

Le cahier des charges était dés plus succinct :

  • Créer un outil de navigation permettant de parcourir une série d'images.
  • Les images doivent être visibles si le javascript est désactivé.
  • Ne pas faire une usine à gaz.
  • Ne pas y passer plus d'une heure.

SimpleSlide étant un projet qui se veut donc simple, codé rapidement et qui surtout ne cherche pas à pouvoir être utilisé tel que le production.

  • Tester sur Firefox 15, Opera 12.02, Chromium 21, Web 3.4.3.
  • Ne fonctionne pas Sous IE 9 (la liste de ce qu'il ne comprend pas serait trop longue).
  • Ne fonctionne pas sous Safari 5, qui ne comprend pas la méthode bind.

Démonstration

Code

<!-- Boutons de navigation -->
<p id="navigator" style="display: none">
    <button value="first"><<</button>
    <button value="previous"><</button>
    <button value="next">></button>
    <button value="last">>></button>
</p>

<!-- Liste des images -->
<ul id="slides">
    <li><img src="2.jpg" alt="wallpaper"></li>
    <li><img src="16.jpg" alt="wallpaper"></li>
    <li><img src="94.jpg" alt="wallpaper"></li>
    <li><img src="159.jpg" alt="wallpaper"></li>
    <li><img src="164.jpg" alt="wallpaper"></li>
    <li><img src="168.jpg" alt="wallpaper"></li>
</ul>

<!-- Boutons d'animation -->
<p id="player" style="display: none">
    <button value="play">►</button>
    <button value="pause">❙❙</button>
    <button value="stop">◼</button>
</p>

<!-- Création des objets javascript -->
<script>
    var slides = new Slides(document.getElementById('slides')),
        nav = new Navigator(document.getElementById('navigator'), slides),
        play = new Player(document.getElementById('player'), slides);
</script>
Code HTML.
/**
 *  Visionneuses d'images (très) simple.
 *
 *  @constructor
 *  @param {HTMLElement} el Elément HTML portant les images.
 */
var Slides = function (el)
{
    /**
     *  Elément HTML portant les images.
     *  @private
     *  @type HTMLElement
     */
    this.element = el;
    
    /**
     *  Nombre d'élément dans la visionneuse
     *  @type int
     */
    this.size = el.children.length || 0;
    
    /**
     *  Position actuelle de la visionneuse.
     *  @private
     *  @type int
     */
    this.currentPosition = -1;
    
    /**
     *  Gestionnaire de lecture
     *  @type Player
     */
    this.player = null;
    
    /**
     *  Gestionnaire de navigation.
     *  @type Navigator
     */
    this.navigator = null;
    
    // Appliquer les styles de la visionneuse et afficher la première image.
    this.element.classList.add('slides');
    this.goTo(0);
};

Slides.prototype = 
{
    // fix constructor
    constructor: Slides,
    
    /**
     *  Retourne le nombre d'images dans la visionneuse.
     *
     *  @return {int}
     */
    count: function () 
    {
        return this.size;
    },
    
    /**
     *  Aller à la première image de la visionneuse.
     *
     *  @return {int} La position actuelle de la visionneuse ou -1 si on ne peut
     *  pas se rendre sur la première image ou si la visionneuse est déjà positionné
     *  sur cette image.
     */
    first: function () 
    {
        return this.goTo(0);
    },
    
    /**
     *  Aller à l'image précédant l'image actuellement affichée.
     *
     *  @return {int} La nouvelle position de la visionneuse ou -1 si on ne peut
     *  pas se rendre sur l'image précédente.
     */
    previous: function () 
    {
        return this.goTo(this.currentPosition - 1);
    },
    
    /**
     *  Récupérer la position actuelle de la visionneuse.
     *
     *  @return {int}
     */
    current: function () 
    {
        return this.currentPosition;
    },
    
    /**
     *  Aller à l'image suivant l'image actuellement affichée.
     *
     *  @return {int} La nouvelle position de la visionneuse ou -1 si on ne peut
     *  pas se rendre sur l'image suivante.
     */
    next: function () 
    {
        return this.goTo(this.currentPosition + 1);
    },
    
    /**
     *  Aller à la dernière image du visionneuse.
     *
     *  @return {int} La position actuelle du visionneuse ou -1 si on ne peut
     *  pas se rendre sur la dernière image ou si la visionneuse est déjà positionnée
     *  sur cette image.
     */
    last: function () 
    {
        return this.goTo(this.size - 1);
    },
    
    /**
     *  Aller à une image spécifique de la visionneuse.
     *
     *  @param {int} idx Position de l'image dans la visionneuse.
     *  @return {int} La nouvelle position de la visionneuse ou -1 si on ne peut pas
     *  se rendre sur cette image ou si la visionneuse est déjà positionnée à
     *  cet endroit.
     */
    goTo: function (idx) 
    {
        if (this.size === 0 || idx < 0 || idx >= this.size || idx === this.currentPosition) {
            return -1;
        }
        
        var previousPosition = this.currentPosition,
            prev, curr, el;
            
        this.currentPosition = idx;
        
        if (previousPosition !== -1) {
            prev = this.element.children[previousPosition];
            prev.classList.remove('current');
            if ( el = prev.previousElementSibling ) {
                el.classList.remove('before');
            }
            if ( el = prev.nextElementSibling ) {
                el.classList.remove('after');
            }
        }
        
        curr = this.element.children[this.currentPosition];
        curr.classList.add('current');
        if ( el = curr.previousElementSibling ) {
            el.classList.add('before');
        }
        if ( el = curr.nextElementSibling ) {
            el.classList.add('after');
        }

        return this.currentPosition;
    }
};

/**
 *  Objet de gestion de lecture.
 *
 *  @constructor
 *  @param {HTMLElement} Elément portant les boutons d'actions.
 *  @param {Object} Objet à animer qui doit posséder des méthodes ``first``,
 *  ``current`` et ``next``
 *  @param {int} [time = 3000] Temps en millisecondes entre les animations.
 */
var Player = function (el, playable, time) 
{
    if (!playable.first || !playable.next || !playable.current) {
        throw new Error('playable must have "first", "next" and "current" methods');
    }
    
    /**
     *  Elément portant les boutons d'actions.
     *
     *  @private
     *  @type HTMLElement
     */
    this.element = el;
    
    /**
     *  Objet cible de l'animation.
     *
     *  @private
     *  @type Object
     */
    this.target = playable;
    
    /**
     *  Flag indiquant l'état activé / désactivé de l'objet.
     *
     *  @private
     *  @type Boolean
     */
    this.disabled = false;
    
    /**
     *  Entre entre chaque animation.
     *
     *  @private
     *  @type int
     */
    this.time = time || 3000;
    
    /**
     *  Identifiant du timer.
     *
     *  @private
     *  @type ID
     */
    this.timer = null;
    
    /**
     *  Statut de l'animation (stop|play|pause).
     *
     *  @private
     *  @type String
     */
    this.status = 'stop';
    
    /**
     *  Binding de la méthode d'appel du timer.
     *
     *  @type Function
     */
    this.onIntervalBinding = this.onInterval.bind(this);
    
    // back reference
    this.target.player = this;
    
    // Afficher l'élément, utile uniquement si le javascript est activé
    this.element.style.display = 'block';
    
    // Ecoute des clicks et mise à jour de l'état des boutons.
    this.element.addEventListener('click', this.onClick.bind(this), false);
    this.update();
};

Player.prototype = 
{
    // Fix constructor
    constructor: Player,
    
    /**
     *  Ecouteur de click sur les boutons d'animation.
     *
     *  @param {Event} e Evénement DOM.
     */
    onClick: function (e)
    {
        if (e.target.nodeName.toLowerCase() !== 'button') {
            return;
        }
        this[e.target.value]();
    },
    
    /**
     *  Méthode d'appel du timer.
     */
    onInterval: function () 
    {
        var pos = this.target.next();
        if (pos === this.target.count() - 1) {
            this.stop();
        }
    },
    
    /**
     *  Activer la gestion de l'animation.
     */
    enable: function () 
    {
        if (this.disabled) {
            this.disabled = false;
            this.update();
        }
    },
    
    /**
     *  Désactiver la gestion de l'animation.
     */
    disable: function () 
    {
        if (!this.disabled) {
            this.stop();
            this.disabled = true;
            this.update();
        }
    },
    
    /**
     *  Lancer l'animation.
     */
    play: function () 
    {
        if (this.disabled || this.status === 'play') {
            return;
        }
        this.stop();
        this.status = 'play';
        if (this.target.navigator) {
            this.target.navigator.disable();
        }
        this.timer = window.setInterval(this.onIntervalBinding, this.time);
        if (this.target.current() === this.target.count() - 1) {
            this.target.first();
        }
        this.update();
    },
    
    /**
     *  Mettre en  pause l'animation.
     */
    pause: function () 
    {
        if (this.disabled || this.status === 'pause') {
            return;
        }
        this.stop();
        this.status = 'pause';
        this.update();
    },
    
    /**
     *  Arrêter l'animation.
     */
    stop: function () 
    {
        if (this.disabled || this.status === 'stop') {
            return;
        }
        if (this.timer !== null) {
            window.clearInterval(this.timer);
            this.timer = null;
        }
        this.status = 'stop';
        if (this.target.navigator) {
            this.target.navigator.enable();
        }
        this.update();
    },
    
    /**
     *  Mettre à jour l'état des boutons d'animations.
     */
    update: function () 
    {
        this.element.children[0].disabled = this.status === 'play' || this.disabled;
        this.element.children[1].disabled = this.status === 'pause' || this.status === 'stop' || this.disabled;
        this.element.children[2].disabled = this.status === 'stop' || this.disabled;
    }
};

/**
 *  Objet de gestion de la navigation entre images.
 *
 *  @constructor
 *  @param {HTMLElement} el Elément portant les boutons de navigation.
 *  @param {Object} navigable Objet à travers lequel naviguer et qui doit posséder
 *  des méthodes``first``, ``previous``, ``current``, ``next`` et ``last``.
 */
var Navigator = function (el, navigable) 
{
    if (!navigable || !navigable.first || !navigable.previous || !navigable.current
            || !navigable.next || !navigable.last) {
        throw new Error('playable must have "first", "previous", "current", ' +
            '"next" and "last" methods');
    }
    
    /**
     *  Elément HTML portant les boutons de navigation.
     *
     *  @private
     *  @type HTMLElement
     */
    this.element = el;
    
    /**
     *  Elément navigable.
     *
     *  @private
     *  @type Object
     */
    this.target = navigable;
    
    /**
     *  Flag indiquant l'état activé / désactivé de l'objet.
     *
     *  @private
     *  @type Boolean
     */
    this.disabled = false;
    
    // back reference
    this.target.navigator = this;
    
    // Afficher l'élément, utile uniquement si le javascript est activé
    this.element.style.display = 'block';

    // Ecoute des clicks et mise à jour de l'état des boutons.
    this.element.addEventListener('click', this.onClick.bind(this), false);
    this.update();
};

Navigator.prototype = 
{
    // Fix constructor
    constructor: Navigator,
    
    /**
     *  Ecouteur d'événement click sur l'élément navigateur.
     *  
     *  @param {Event} e Evénément DOM.
     */
    onClick: function (e) {
        if (e.target.nodeName.toLowerCase() !== 'button') {
            return;
        }
        if (this.target[e.target.value]() > -1) {
            this.update();
        }
    },
    
    /**
     *  Activer la navigation
     */
    enable: function () 
    {
        if (this.disabled) {
            this.disabled = false;
            this.update();
        }
    },
    
    /**
     *  Désactiver la navigation
     */
    disable: function ()
    {
        if (!this.disabled) {
            this.disabled = true;
            this.update();
        }
    },
    
    /**
     *  Mettre à jour l'état des boutons de navigation.
     */
    update: function () 
    {
        var current = this.target.current(),
            past = current === 0 || this.disabled,
            futur = (current === this.target.count() - 1) || this.disabled;
        this.element.children[0].disabled = past;
        this.element.children[1].disabled = past;
        this.element.children[2].disabled = futur;
        this.element.children[3].disabled = futur;
    }
};
Code javascript.
/*
Styles appliqués à la visionneuse d'images.
*/
.slides {
    position: relative;
    padding: 0;
    margin: 0;
    overflow: hidden;
    /* tailles, à définir en fonction de celle des images */
    width: 460px;
    height: 288px;
    border: 5px solid #fff;
    /* décorations */
    border-collapse: separate;
    box-shadow: 5px 5px rgba(0, 0, 0, 0.2);
}
.slides > li {
    display: none;
    list-style: none;
    /* box */
    position: absolute;
    width: 100%;
    height: 100%;
    /* effects */
    transition: transform 0.3s ease-out 0s;
}
.slides > li > img {
    width: 100%;
    height: auto;
}
.slides > .before {
    display: block;
    transform: translate(-100%);
}
.slides > .current {
    display: block;
    transform: translate(0);
}
.slides > .after {
    display: block;
    transform: translate(100%);
}
Code CSS.