2011/07/29

Lectura de los ficheros de un directorio con ordenación en PHP

Un apunte rápido para hoy.

Si necesitáis leer los nombres de los ficheros de un directorio en función de un filtrado arbitrario y recorrerlos en orden de fecha de creación, se puede hacer un bucle muy sencillo usando la maravillosa función glob que combinada con la función usort que permite especificar una función para ordenar, al final tendremos un array ordenado según el criterio definido.

Al turrón!
$ruta = "directorio/*.*"; // Cambiad la máscara de ficheros por la que deseéis
$arr = glob( $ruta ); 
usort( $arr, create_function( '$a,$b', 'return filemtime($a) - filemtime($b);' ) );

Rápido y sencillo, como debe ser.

2011/07/27

jQuery FRoll plugin - Preview de vídeos sin necesidad de cargarlos


La carga de un objeto de tipo vídeo es una operación realmente costosa y pesada respecto a rendimiento de una página. Máxime si estamos haciendo una galería de vídeos donde se muestran varios vídeos a la vez.

Una primera aproximación para aligerar esa carga es lo que se ha hecho toda la vida mediante la utilización de thumbnails de los vídeos, pero yo al menos siempre me quedo con la sensación de que quiero algo más... un triste fotograma de un vídeo no me saca de pobre y más si es un fotograma automatizado que puede que coincida con una pantalla en negro.

Pero... y si tuviéramos varios fotogramas de un mismo vídeo. Se podrían poner uno a continuación del otro para hacer una secuencia, pero es algo que se antoja "artificial"... lo ideal sería que dichos fotogramas se reprodujeran en sucesión antes de la carga real del vídeo de modo que solo se muestre el vídeo en sí bajo petición por parte del usuario.

Ahí es donde entra FRoll, un plugin 100% hecho por mí, haciéndonos la vida un poco más sencilla y más si el vídeo en cuestión está alojado en youtube, que hace el trabajo de sacar fotogramas de los vídeos por nosotros (sólo 3, pero qué le vamos a hacer).

Una vez incluido este plugin, tendremos disponible el método .froll() que podremos llamar sobre las imágenes que deseemos convertir en previsualizaciones de vídeos. El nombre de los fotogramas se deriva a partir del nombre de la imagen original mediante sencillas reglas de transformación y ya se proporcionan reglas para los escenarios más comunes que son el de imágenes alojadas en youtube, thumbnail en local y fotogramas en remoto en youtube e imagen + fotogramas en local. Usando dichos ejemplos es fácil adaptar el comportamiento a las necesidades de cada uno.

También soporta el la delegación del evento de click para permitirnos completar acciones sobre la imagen original y que el uso del plugin sea totalmente transparente tanto para el usuario como para el desarrollador.

Para ver el plugin en acción, como es costumbre, podéis hacerlo en mi fiddle o en la página del plugin en el sitio de jQuery.


Y ahora, el código del plugin, que está comentado hasta el aburrimiento para que no se pierda nadie, y ya sabéis, si lo usáis, mejorais, arregláis, hacédmelo saber ;)
/**
 * jquery.froll-0.1.js - Fancy Image Roll
 * ==========================================================
 * (C) 2011 José Ramón Díaz - jrdiazweb@gmail.com
 *
 * http://3nibbles.blogspot.com/2011/07/plugin-jquery-sliding-buttons.html
 * http://plugins.jquery.com/project/froll
 *
 * FRoll is a jQuery plugin to simplify the task of providing a simple and
 * efficient way to expand the image information of a picture using a sequence
 * of images.
 *
 * The direct use of this plugin is to provide a preview of a youtube video
 * using the Google API. In that case it presents a sucession of three
 * different moments of the video in a smooth sucession when mouse enters into
 * the image.
 *
 * Everything is self-contained. No need of extra CSS or complex controls,
 * just call the method over the image and you are ready.
 *
 * INSTANTIATION
 * Just call the ".froll()" method over the images selector.
 *
 *     $('.videoCaptionImg').froll( [ options_object ] );
 *
 * OPTIONS
 *     - transform  $.froll.youtube  Array that defines [0] as the regex to apply
 *                                   to src and [1] as the resulting string with
 *                                   {number} placeholder for the animation images.
 *     - frames     [1, 2, 3],       Array with the {number} of each frame of the animation
 *     - width      null,            Width of the animation frames.  Null = automatic
 *     - height     null,            Height of the animation frames. Null = automatic
 *     - speed      750,             Fade animation speed
 *     - time       1500,            Time between frames
 *     - click      function(taget)  Click callback function. Defaults to trigger click
 *                                   event over the container <a> of the img.
 *
 * PUBLIC API
 *     -  $.froll.stop()    Stops current animation and hides the preview
 *
 * HELPER CONSTANTS
 *     $.froll.youtube       Transform array that converts origin src stored in youtube
 *                           (http://img.youtube.com/vi/<video_ID>/0.jpg) into youtube previews.
 *     $.froll.youtubeLocal  Transform array that converts local stored captions with name the
 *                           id of the video, into youtube previews.
 *     $.froll.local         Transform array that converts local stored captions into local
 *                           stored previews with the same name but ended with "_<number>"
 *                           in the same directory than caption.
 *
 * CSS CLASSES
 *     - Container: #froll-overlay
 *     - Frames:    .froll-frame
 *
 * Legal stuff
 *     You are free to use this code, but you must give credit and/or keep header intact.
 *     Please, tell me if you find it useful. An email will be enough.
 *     If you enhance this code or correct a bug, please, tell me.
 */
(function( $ ) {

    ///////////////////////////////////////////////////////////////////////////////
    // Private members
    ///////////////////////////////////////////////////////////////////////////////

    var busy      = false,
        overlay   = null,
        frames    = [],
        frame     = -1,
        lframe    = 0,
        options   = {},
        target    = null,
        src       = "",
        tickTimer = null,
        imgPreloader = new Image(),
        //isIE6 = $.browser.msie && $.browser.version < 7 && !window.XMLHttpRequest,

        // ========================================================================
        // Starts the animation
        _start = function() {
            _stop(); // Hides current animation (if any)

            if( !target.attr('src') ) return; // No image src

            // Gets the options and src transform
            options = target.data('froll');
            src = target.attr('src').replace( options.transform[0], options.transform[1] );

            // Moves the overlay to the target position
            var pos = _getPos(target);
            overlay.css({
                'position'  : 'absolute',
                'left'      : pos.left,
                'top'       : pos.top,
                'width'     : pos.width,
                'height'    : pos.height,
                'zIndex'    : 9999,
                'background': 'transparent'
                //,'border': '1px solid red'
            }).show();

            // ========================================================================
            // Starts the frames preload chain loading first frame
            lframe = 0; // Frame being loaded
            if(!imgPreloader) imgPreloader = new Image();
            imgPreloader.onerror = function() { _error(); };
            imgPreloader.onload  = _preloadCompleted;

            imgPreloader.src = src.replace( /\{number\}/, ""+options.frames[lframe] );
            if(imgPreloader.complete) _preloadCompleted(); // Cached images don't fire onload events
        },

        // ========================================================================
        // Gracefully stops the animation
        _stop = function() {
            clearInterval(tickTimer); // Disables the timer
            imgPreloader.onerror = imgPreloader.onload = null;
            if( target && overlay.is( ':visible' ) )
                overlay.hide().empty();   // Hides the overlay and deletes the frames

            frames = [];
            frame  = -1;
            //target = options = null;
            busy   = false;
        },

        // ========================================================================
        // Frame image load error
        _error = function() {
            alert( "Error loading image at " + src );
        },

        // ========================================================================
        // Function called on animation click
        _click = function(e) {
            if( typeof options.click !== 'undefined' )
                options.click(target);
        },

        // ========================================================================
        // Image preload complete event
        _preloadCompleted = function() {
            // Gets default image dimensions
            if( !options.width )  options.width  = overlay.width();  //imgPreloader.width;
            if( !options.height ) options.height = overlay.height(); //imgPreloader.height;

            // Creates frame ima
            $("<img />").attr({
                'id'   : 'froll-frame-' + lframe,
                'class': 'froll-frame',
                'src'  : imgPreloader.src
            }).css({
                'position'  : 'absolute',
                'display'   : 'block',
                'left'      : '0px',
                'top'       : '0px',
                'width'     : options.width+'px',
                'height'    : options.height+'px',
                'zIndex'    : lframe+1,
                'opacity'   : 0
                //,'visibility': 'hidden'
            }).appendTo( overlay );

            // Shows first frame
            if( lframe == 0 )
            {
                _tick();
                tickTimer = setInterval( _tick, options.time );
            }

            // Preloads next frame
            frames[ lframe++ ] = 1; // Marks frame as done
            if( lframe < options.frames.length )
            {
                // Intermediate frame
                imgPreloader.src = src.replace( /\{number\}/i, options.frames[lframe] );
                if(imgPreloader.complete) _preloadCompleted(); // Cached images don't fire onload events
            }
            else
                imgPreloader.onerror = imgPreloader.onload = null; // Last frame
        },

        // ========================================================================
        // Shows next frame
        _tick = function() {

            var l = options.frames.length - 1;
            var children = overlay.children();

            // Animates next frame
            if( frame == -1 )
            {
                // First run
                children.eq( 0 ).css('opacity', 0).stop( true, true ).animate( { 'opacity': 1 }, options.speed );
                frame = 0;
            }
            else if( frame == 0 )
            {
                // First frame (after a full run)
                for(var i = 1; i < l; i++) children.eq( i ).css( 'opacity' , 0); // Hides all but first and last frames
                children.eq( 0 ).css( 'opacity', 1 ).show(); // Shows first frame (bellow last frame)
                children.eq( l ).stop( true, true ).animate( { 'opacity': 0 }, options.speed );
            }
            else if( frame <= l )
            {
                // Intermediate frame
                var next = children.eq( frame );
                if(next)
                    next.css('opacity', 0).stop( true, true ).animate( { 'opacity': 1 }, options.speed );
            }
            else
            {
                // The last frame. Resets animation to show first frame and hide the last one
                children.eq( 0 ).css( 'opacity', 1 );
                children.eq( l ).stop( true, true ).animate( { 'opacity': 0 }, options.speed );
            }
            frame = (frame+1) % (l+1);
        },

        // ========================================================================
        // Helper function to get the exact obj position in the page
        _getPos = function(obj) {

            var pos = obj.offset();

            pos.top   += parseInt( obj.css( 'paddingTop' )       , 10 ) || 0;
            pos.left  += parseInt( obj.css( 'paddingLeft' )      , 10 ) || 0;

            pos.top   += parseInt( obj.css( 'border-top-width' ) , 10 ) || 0;
            pos.left  += parseInt( obj.css( 'border-left-width' ), 10 ) || 0;

            pos.width  = obj.width();
            pos.height = obj.height();

            return pos;
        };


    ///////////////////////////////////////////////////////////////////////////////
    // Public members
    ///////////////////////////////////////////////////////////////////////////////

    // ========================================================================
    // Instantiation. Called on every object of the supplied selector
    $.fn.froll = function( obj ) {
        if (!$(this).length) {
            return this;
        }

        if( $(this).data( 'froll' ) )
        {
            // Object already initialized. Starts the animation over it simulating a click
            if($(this).click) $(this).click();
        }
        else
        {
            // Object not initialized. Sets data and binds events
            $(this)
                .data( 'froll', $.extend( $.fn.froll.defaults, obj ) )
                .unbind( 'mouseenter' )
                .bind( 'mouseenter', function(e) {
                    var self = $(this);
                    e.preventDefault();

                    if (busy && self !== target) _stop(); // Stops current animation
                    busy = true;
                    //var rel = self.attr('rel') || '';
                    target = self;

                    _start(); // Starts the animation over target element
                    return;
                });
        }

        return this;
    };


    // ========================================================================
    // Container class for the public interface
    $.froll = function(obj) {  };

    // ========================================================================
    // Inits the components needed for the animation overlay
    $.froll.init = function() {
        if ( $( '#froll-overlay' ).length ) {
            return;
        }

        // Components
        $('body').append(
            overlay    = $( '<div id="froll-overlay"></div>' )
        );

        // Animation controls events
        overlay.mouseleave( _stop );
        overlay.click( _click );

        return this;
    };

    // ========================================================================
    // Stops current animation and hides overlay
    $.froll.stop = function() { _stop(); };

    // ========================================================================
    // Sample transformation arrays
    $.froll.youtube      = [ /.*\/(.*)\/0\.(jpg|gif|png|bmp|jpeg)(.*)?/i, 'http://img.youtube.com/vi/$1/{number}.$2' ]; // Caption image is located in youtube (http://img.youtube.com/vi/<video_ID>/0.jpg)
    $.froll.youtubeLocal = [ /.*\/(.*)\.(jpg|gif|png|bmp|jpeg)(.*)?/i   , 'http://img.youtube.com/vi/$1/{number}.$2' ]; // Caption image is located elsewhere but caption image name is the youtube video_ID
    $.froll.local        = [ /(.*)\/(.*)\.(jpg|gif|png|bmp|jpeg)(.*)?/i , '$1/$2_{number}.$3$4' ];                      // Caption image is located elsewhere and frames are in format "originalImage_<frame>.jpg" in the same directory

    // ========================================================================
    // Froll options defaults
    $.fn.froll.defaults = {
        transform  : $.froll.youtube, // Array that defines [0] as the regex to apply to src and [1] as the resulting string with {number} placeholder for the animation images
        frames     : [1, 2, 3],       // Array with the {number} of each frame of the animation
        width      : null,            // Width of the animation frames.  Null = automatic frame image width
        height     : null,            // Height of the animation frames. Null = automatic frame image height
        speed      : 750,             // Fade animation speed
        time       : 1500,            // Time between frames

        click      : function(taget) { $(target).closest('a').click(); } // Click callback function
    };

    // ========================================================================
    // Inits the animation overlay on DOM ready
    $(document).ready(function() {
        $.froll.init();
    });

})( jQuery );

Y ahora, la versión minimizada, que se queda en 2.5Kb (aún menos gzipeada, claro)
/**
 * jquery.froll-0.1.js - Fancy Image Roll - (C) 2011 José Ramón Díaz - jrdiazweb@gmail.com
 * http://3nibbles.blogspot.com/2011/07/jquery-froll-plugin-preview-de-videos.html
 * http://plugins.jquery.com/project/froll
 * You are free to use this code, but you must give credit and/or keep header intact.
 */
(function(a){var k=!1,d=null,g=-1,h=0,b={},e=null,i="",m=null,c=new Image,p=function(){j();if(e.attr("src")){b=e.data("froll");i=e.attr("src").replace(b.transform[0],b.transform[1]);var a=o(e);d.css({position:"absolute",left:a.left,top:a.top,width:a.width,height:a.height,zIndex:9999,background:"transparent"}).show();h=0;c||(c=new Image);c.onerror=function(){alert("Error loading image at "+i)};c.onload=l;c.src=i.replace(/\{number\}/,""+b.frames[h]);c.complete&&l()}},j=function(){clearInterval(m);c.onerror= c.onload=null;e&&d.is(":visible")&&d.hide().empty();g=-1;k=!1},q=function(){typeof b.click!=="undefined"&&b.click(e)},l=function(){if(!b.width)b.width=d.width();if(!b.height)b.height=d.height();a("<img />").attr({id:"froll-frame-"+h,"class":"froll-frame",src:c.src}).css({position:"absolute",display:"block",left:"0px",top:"0px",width:b.width+"px",height:b.height+"px",zIndex:h+1,opacity:0}).appendTo(d);h==0&&(n(),m=setInterval(n,b.time));h++;h<b.frames.length?(c.src=i.replace(/\{number\}/i,b.frames[h]), c.complete&&l()):c.onerror=c.onload=null},n=function(){var a=b.frames.length-1,f=d.children();if(g==-1)f.eq(0).css("opacity",0).animate({opacity:1},b.speed),g=0;else if(g==0){for(var c=1;c<a;c++)f.eq(c).css("opacity",0);f.eq(0).css("opacity",1).show();f.eq(a).animate({opacity:0},b.speed)}else g<=a?(f=f.eq(g))&&f.css("opacity",0).animate({opacity:1},b.speed):(f.eq(0).css("opacity",1),f.eq(a).animate({opacity:0},b.speed));g=(g+1)%(a+1)},o=function(a){var b=a.offset();b.top+=parseInt(a.css("paddingTop"), 10)||0;b.left+=parseInt(a.css("paddingLeft"),10)||0;b.top+=parseInt(a.css("border-top-width"),10)||0;b.left+=parseInt(a.css("border-left-width"),10)||0;b.width=a.width();b.height=a.height();return b};a.fn.froll=function(b){if(!a(this).length)return this;a(this).data("froll")?a(this).click&&a(this).click():a(this).data("froll",a.extend(a.fn.froll.defaults,b)).unbind("mouseenter").bind("mouseenter",function(b){var c=a(this);b.preventDefault();k&&c!==e&&j();k=!0;e=c;p()});return this};a.froll=function(){}; a.froll.init=function(){if(!a("#froll-overlay").length)return a("body").append(d=a('<div id="froll-overlay"></div>')),d.mouseleave(j),d.click(q),this};a.froll.stop=function(){j()};a.froll.youtube=[/.*\/(.*)\/0\.(jpg|gif|png|bmp|jpeg)(.*)?/i,"http://img.youtube.com/vi/$1/{number}.$2"];a.froll.youtubeLocal=[/.*\/(.*)\.(jpg|gif|png|bmp|jpeg)(.*)?/i,"http://img.youtube.com/vi/$1/{number}.$2"];a.froll.local=[/(.*)\/(.*)\.(jpg|gif|png|bmp|jpeg)(.*)?/i,"$1/$2_{number}.$3$4"];a.fn.froll.defaults={transform:a.froll.youtube, frames:[1,2,3],width:null,height:null,speed:750,time:1500,click:function(){a(e).closest("a").click()}};a(document).ready(function(){a.froll.init()})})(jQuery);

Actualización. Añadida la clase CSS para cambiar el cursor a tipo "mano" al hacer hover. Ver en el jsfiddle.

2011/07/21

Detectar la parte inferior de la página con Javascript

Cómo saber si el usuario ha hecho scroll hasta el final de la página? (así de pronto se me ocurre que puede ser útil para EULAs y compañía).

Pues fácil, con este fragmento de código jQuery (que recomiendo se añada al final de la página o en el evento de domready)

Un código HTML de ejemplo sería el siguiente:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
 <head>
  <title>Javascript - Detecting the bottom of the page</title>
 
  <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.5.1/jquery.min.js"></script>
  <script type="text/javascript">
 
   $(window).scroll(function() {
 
    $('#notification2').html('document.height = ' + document.height);
    $('#notification3').html('window.pageYOffset + window.innerHeight = ' + (window.pageYOffset + window.innerHeight));
 
    if(document.height == window.pageYOffset + window.innerHeight)
    {
     // hit bottom
     $('#notification1').html('HIT BOTTOM');
    }
    else
    {
     $('#notification1').html('NOT AT BOTTOM');
    }
   });
 
  </script>
 
  <style type="text/css">
 
   body
   {
    margin:0px;
    padding:0px;
   }
 
   .box
   {
    width:400px;
    height:400px;
    margin:0px auto;
    background:#ccc;
   }
 
   .notification
   {
    position:fixed;
    color:#777;
    width:auto;
    height:auto;
    background:#ccc;
    padding:10px;
    font-family:Arial, Helvetica;
    font-weight:bold;
 
   }
 
  </style>
 
 </head>
 <body>
 
 <div class="notification" id="notification1">NOT AT BOTTOM</div>
 <div class="notification" style="top:50px;" id="notification2">document.height = </div>
 <div class="notification" style="top:100px;" id="notification3">window.pageYOffset + window.innerHeight = </div>
 
 
  <div class="box"></div>
  <div class="box"></div>
  <div class="box"></div>
  <div class="box"></div>
  <div class="box"></div>
  <div class="box"></div>
 
 </body>
</html>

2011/07/15

Plugin jQuery Sliding Buttons

Después de algún tiempo de diseño cociendo algunas cosas, he salido de la cueva y os presento otro plugin de jQuery totalmente hecho por mí. Ante vosotros está el magnífico Sliding Buttons!

          

Este plugin transforma un inocente span o div con dos contenedores de tipo inline dentro en un vistoso botón cuyos contenidos (primer contendor) aparece y desaparece al pasar el ratón sobre él. Soporta eventos y funciona!

Ahí va el jsFiddle de rigor y como siempre, si encuentras algún bug o lo mejoras, decídmelo, y si lo usas, cuéntamelo también, que al menos no tenga sensación de haber perdido el tiempo.


Ejemplo

Código javascript
/**
 * jquery.slideButton-0.1.js - Slide Button plugin for jQuery
 * ==========================================================
 *  (C) 2011 José Ramón Díaz - jrdiazweb@gmail.com
 *
 * http://3nibbles.blogspot.com/2011/07/plugin-jquery-sliding-buttons.html
 *
 * INSTANTIATION
 * Any container with the .slideButton class will be initialized
 * on domReady event using the options defined in object
 * "slideButtonDefaults". This feature can be disabled by setting
 * the variable "slideButtonAutoload" to the false value.
 *
 * Manual instantiation
 *     $('.slideButton').slideButton( [ options_object ] );
 *
 * OPTIONS
 *     - direction    : 'left',   Slide direction "left" = LtR, "right" = RtL
 *     - border       : '3px',    Border width == difference in height of control(>) and contents(<)
 *     - radius       : '20px',   Border radius of the slideButton
 *     - height       : '36px',   Maximum height of the slideButton. Includes all borders
 *     - width        : '70px',   Minimum width. Applied to control
 *     - deployedWidth: '225px',  Maximum width. Applied to container and deployed contents
 *     - inAnimation  : 'swing',  Function used to deploy (slide in) the elements. For more functions needs the easing packs
 *     - outAnimation : 'swing',  Function used to hide (slide out) the elements. For more functions needs the easing packs
 *     - inSpeed      : 500,      deploy effect duration
 *     - outSpeed     : 300,      hide effect duration
 *     - onClick      : null,     button click trigger callback function
 *     - onBlur       : null,     blur (slide out) trigger callback function
 *     - onDeploy     : null,     deploy (slide in) trigger callback function
 *     - onHide       : null,     hide (end of slide out) trigger callback function
 *
 * API
 *     - show()    Deploys the sliding contents
 *     - hide()    Hides the sliding contents
 *     - lock()    Locks the sliding contents open
 *     - locked    Indicates that the control is locked
 *     - deployed  Indicates that the control is deployed
 *
 * CLASSES
 *     - Container: .slideButton, .deployed, .clicked
 *     - Button:    .button
 *     - Content:   .content
 *
 * HTML format
 *     *
 * Valid HTML content and control containers are any inline element (a, span, etc...).
 * Container must be div or span.
 *
 * Legal stuff
 *     You are free to use this code, but you must give credit and/or keep header intact.
 *     Please, tell me if you find it useful. An email will be enough.
 *     If you enhance this code or correct a bug, please, tell me.
 */
var slideButtonDefaults = {};   // The default options used for all auto loading sliding buttons
var slideButtonAutoload = true; // Enables the autoinstantiation feature for .slidingButton

(function( $ ) {
    // Private members
    function SlideButton( el, options ) {
        this.container = $(el);
        this.cA = null;
        this.cB = null;

        //this.el.attr('locked', 'off');

        // States control
        this.locked   = false;
        this.deployed = false;

        // Default options
        this.options = {
                direction    : 'right',   // Slide direction "left" = LtR, "right" = RtL
                border       : '3px',    // Border width == difference in height of control(>) and contents(<)
                radius       : '20px',   // Border radius of the slideButton
                height       : '36px',   // Maximum height of the slideButton. Includes all borders
                width        : '70px',   // Minimum width. Applied to control
                deployedWidth: '225px',  // Maximum width. Applied to container and deployed contents

                inAnimation  : 'swing',  // Function used to deploy (slide in) the elements. For more functions needs the easing packs
                outAnimation : 'swing',  // Function used to hide (slide out) the elements. For more functions needs the easing packs
                inSpeed      : 300,      // deploy effect duration
                outSpeed     : 200,      // hide effect duration

                onClick      : null,     // button click trigger callback function
                onBlur       : null,     // blur (slide out) trigger callback function
                onDeploy     : null,     // deploy (slide in) trigger callback function
                onHide       : null,     // hide (end of slide out) trigger callback function
        };

        slideButtonDefaults = this.options; // Sets the initial value of global default options

        this.setOptions( options );
        this.initialize();
    }

    // Instantiation
    $.fn.slideButton = function( options ) {
        return new SlideButton(this.get(0) || $('<span />') || $('<div />'), options);
    };

    // Public Members
    SlideButton.prototype = {
        // Plugin functions
        killerFn: null,

        initialize: function() {
            var me, o, c, children, cA, cB, h1, h2, w1, w2, w3, r1, r2, b;
            me = this;
            o  = me.options;
            c  = me.container;

            // Sets objects and variables initial values
            children = c.children();
            cA = me.cA = $( children[0] );
            cB = me.cB = $( children[1] );

            // Widths and heights
            b  =   parseInt(o.border);                  // Border
            h1 =   parseInt( o.height ) + 'px';         // Container/Contents height
            h2 = ( parseInt( h1 ) - (b * 2) ) + 'px'; // Control height
            w1 =   parseInt( o.deployedWidth ) + 'px';  // Container and deployed width
            w2 =   parseInt( o.width ) + 'px';          // Contents width
            w3 = ( parseInt( w2 ) - (b * 2) ) + 'px';  // Control width
            r1 =   parseInt( o.radius ) + 'px';         // External border radius
            r2 = ( parseInt( o.radius ) - b ) + 'px'; // Internal border radius

            // Container modifications
            if( !c.hasClass('slideButton') ) me.container.addClass( 'slideButton' );
            c.width( w1 );
            c.height( h1 );
            //me.container.height(o.height);
            c.css({ 'position': 'relative', 'overflow': 'hidden' });

            // Chidren modifications
            cA.addClass( 'content' ); //o.direction + 'A content' );
            cB.addClass( 'button'  ); //o.direction + 'B button' );

            // Wraps sliding contents into a span
            var text = cA.get(0).innerHTML;
            cA.get(0).innerHTML = '' + text + '';
            cA.find('span:first-child').hide();

            // Sets styles
            c.data( 'initialWidth', w1 ); // Stores the initial width in container data
            cA.width( w2 );
            cB.width( w3 );
            cA.height( h1 );
            cB.height( h2 );
            cA.css({ 'position': 'absolute', 'top': '0px' , 'lineHeight': h1, 'zIndex': 0, 'borderRadius': r1 });
            cB.css({ 'position': 'absolute', 'top': b+'px', 'lineHeight': h2, 'zIndex': 1, 'borderRadius': r2 });

            if(o.direction == 'left')
            {
                cA.css({ 'right': '0px' , 'textAlign': 'left' });
                cB.css({ 'right': b+'px' });
            }
            else
            {
                cA.css({ 'left': '0px' , 'textAlign': 'right' });
                cB.css({ 'left': b+'px' });
            }

            // Killer function set
            this.killerFn = function( e ) {
                // Test if didn't clicked on the slideButton
                var tar = $(e.target);
                if (tar.parents( '.slideButton' ).size() === 0) {
                    me.locked   = false;
                    me.cB.removeClass( 'clicked' );
                    me.hide();
                    me.disableKillerFn();
                }
            };

            // Stores slideButton instance in container
            c.data( 'slideButton', me );

            // Event listeners
            cB.hover( me.show, me.hide );
            cB.click( me.click );
        },

        setOptions: function(options) {
            var o = this.options;
            var c = this.container;

            // Uses container height if no options.height is given
            if(typeof(o.height) === 'undefined' || o.height == null || o.height <= 0)
                if(c.height()) o.height = c.height();

            $.extend(o, options);
        },

        // Slide visualization functions
        show: function() {
            var me;
            if( typeof this.container !== 'undefined' )
                me = this.cB;
            else
                me = $(this);

            // Gets the instance
            var sb = me.parent().data( 'slideButton' );
            if(sb.deployed) return; // It is already deployed
            var o  = sb.options;

            var slidelem = me.prev();
            // Animates the contents of control
            slidelem.stop().animate( { 'width': parseInt(o['deployedWidth']) + 'px' }, o['inSpeed'] ); //, o['inAnimation'] );
            // Animates the contents of content
            slidelem.find( 'span' ).stop( true, true ).fadeIn();

            // Updates the state
            sb.container.addClass('deployed');
            sb.deployed = true;

            // Triggers events
            if( typeof o.onDeploy === 'function' ) o.onDeploy(sb);
        },

        hide: function(force) {
            var me;
            if(typeof this.container !== 'undefined')
                me = this.cB;
            else
                me = $(this);

            if( typeof force !== 'object' && force == true ) me.removeClass('clicked');
            if( me.hasClass('clicked') ) return; // Ignore hide when in clicked (locked) state

            // Gets the instance
            var sb = me.parent().data( 'slideButton' );
            if(!sb.deployed) return; // Already hidden
            var o  = sb.options;

            var slidelem = me.prev();
            // Animates the contents of control
            slidelem.stop().animate( { 'width': parseInt(o.width)+'px' }, o['outSpeed'], o['outAnimation'] );
            // Animates the contents of content
            slidelem.find( 'span' ).stop( true, true ).fadeOut();

            // Updates the state
            sb.container.removeClass( 'deployed' );
            sb.deployed = false;
            sb.locked   = false;

            // Triggers events
            if( typeof o.onHide === 'function' ) o.onHide(sb);
        },

        click: function (ev) {
            var me = $(this);

            // Gets the instance
            var sb = me.parent().data( 'slideButton' );
            var o  = sb.options;
            if( me.hasClass( 'clicked' ) )
            {
                me.removeClass( 'clicked' );
                sb.hide();
                sb.disableKillerFn();
            }
            else
            {
                sb.show();
                sb.enableKillerFn();
                me.addClass( 'clicked' );
            }

            // Triggers events
            if( typeof o.onClick === 'function' ) o.onClick(sb);
        },

        lock: function() {
            var sb = $(this).data( 'slideButton' );
            if(!sb.deployed) sb.show();
            sb.enableKillerFn(); // Hooks the click event on the document to close
            sb.locked = true;
        },

        // Lock killer functions
        enableKillerFn: function() {
            var me = this;
            $(document).bind( 'click', me.killerFn );
        },

        disableKillerFn: function() {
            var me = this;
            $(document).unbind( 'click', me.killerFn );
        },

    };

}(jQuery));


// Autoinitializarion
$(document).ready(function() {
    if(slideButtonAutoload)
        $( '.slideButton' ).each( function ( index, elem ) {
            $(elem).slideButton( slideButtonDefaults );
        });
});


///////////////////////////////////////////////////////////////////////
/*
 * Border-radius jQuery cssHook
 * ==========================================================
 * (C) 2011 José Ramón Díaz - jrdiazweb@gmail.com
 *
 * Instead of using borderRadius cssHook from Brandon Aaron for example
 * (https://github.com/brandonaaron/jquery-cssHooks), I define a custom
 * cssHook for the borderRadius CSS property for the sake of simplicity
 * and to reduce other modules dependencies.
 *
 * If you prefer to use another cssHook from another source (Brandon's
 * one for example), just delete this and include the other cssHook.
 *
 * NOTE: cssHooks needs jQuery v1.4.3 or greater.
 */
(function( $ ){

    var div      = document.createElement('div'),
        divStyle = div.style;

    if ( !$.cssHooks ) {
        // if not, output an error message
        throw("jQuery 1.4.3 or above is required for this plugin to work");
        return;
    }

    div = null; // Avoids IE memory leaks

    $.support.borderRadius =
        divStyle.MozBorderRadius    === ''? 'MozBorderRadius'    :
       (divStyle.msBorderRadius     === ''? 'msBorderRadius'     :
       (divStyle.WebkitBorderRadius === ''? 'WebkitBorderRadius' :
       (divStyle.OBorderRadius      === ''? 'OBorderRadius'      :
       (divStyle.borderRadius       === ''? 'borderRadius'       :
        false))));

    // Border radius will be set only in border-radius compatible "borderRadius" browsers
    if ( $.support.borderRadius && $.support.slideBorderRadius !== "borderRadius" )
    {

        $.cssHooks["borderRadius"]    = {

                get: function( elem, computed, extra ) {
                    return $.css( elem, $.support.borderRadius );
                },
                set: function( elem, value) {
                    elem.style[$.support.borderRadius] = value;
                }

        };

    }

})(jQuery);
CSS
/**
 * Slide Button Styles.
 * ================================================
 * (C) 2011 José Ramón Díaz - jrdiazweb@gmail.com
 *
 * http://3nibbles.blogspot.com/2011/07/plugin-jquery-sliding-buttons.html
 *
 * Slide Button CSS styles. Note that rounded borders is a CSS3 feature,
 * so, it will be represented correctly in CSS3 ready browsers.
 *
 * Legal stuff
 *     You are free to use this CSS, but you must give credit or keep header intact.
 *     Please, tell me if you find it useful. An email will be enough.
 *     If you enhance this code or correct a bug, please, tell me.
 */
.slideButton                                   { font-weight: bold; font-size: 11px; font-family: Arial } /* Container                 */
  .slideButton a                               { text-decoration: none; }                                 /* a components containers   */

  .slideButton .button                         { background-color: #FFFFFF; color: #000000; text-align: center;
                                                 cursor: pointer; text-transform: uppercase; }            /* Control                   */
    .slideButton.deployed .button              { background-color: #9AFF66; color: #FFFFFF;  }            /* Control - Deployed state  */

  .slideButton .content                        { background-color: #36A300; color: #FFFFFF; }             /* Content                   */
    .slideButton.deployed .content             {  }                                                       /* Content - Deployed state  */

/* Depends on contents of the content component */
  .slideButton .button span                    { color: #9AFF66; }                                        /* Control spans             */
    .slideButton.deployed .button span         { color: #36A300; }                                        /* Control spans - Deployed  */
  .slideButton .content span span              { color: #9AFF66; }                                        /* Content spans             */
    .slideButton.deployed .content span        {  }                                                       /* Content spans - Deployed  */

2011/07/07

Introducción a Node

No es un secreto que siempre me han atraido las aplicaciones en tiempo real, pero desgraciadamente en el desarrollo web, la comunicación bidireccional de la información para realimentarse de los datos siempre ha sido con suerte artificiosa.

Afortunadamente, HTML5 ha venido al rescate de todos con la maravillosa implementación de los websockets... pero toda moneda tiene su revés... ningún servidor web soporta el protocolo de websockets, y no solo eso, si no que además, dicho soporte es poco menos que ciencia ficción porque el modelo de funcionamiento de los servidores web basado en petición-respuesta es incompatible con el modelo de funcionamiento de los websockets basado en eventos.

Vamos a matizarlo un poco... poderse, se puede... pero el rendimiento es para echarse a llorar. Existe el paradigma de las 10k conexiones que dice que un servidor web debe ser capaz de servir a 10000 clientes simultáneos. Alguno dirá que 10k conexiones son una barbaridad, pero hay que tener en cuenta que con websockets los eventos tienen una vida muy corta y se pueden realizar muchos por cada página, con lo que poniendo una media de 10 conexiones por página nos da un total de 1000 clientes reales... y si es una página que tiene mucho tráfico, pues nos encontraremos con problemas muy rápido.

La única solución que nos queda es montar un nuevo servidor web dedicado únicamente a websockets y que sea capaz de cumplir con el 10k. A día de hoy hay varias alternativas, unas comerciales y otras libres, pero la mejor sin duda (al menos para mí) es Node.js.

Qué es Node.js? pues aunque cueste creerlo es un servidor de websockets basado en javascript (comorl???). Pues sí, y como muestra la gráfica de pruebas de stress a continuación, se lleva de calle a cualquier otra plataforma.

Para los que tengan curiosidad, la línea verde es Apache sobre Ubuntu que se ve claramente que no llega ni a las 1000 conexiones concurrentes mientras que Node sobrepasa ampliamente las 10k hasta las casi 14k con la misma máquina.

Ahora vienen las pegas ya que tono no es un campo de flores. La programación orientada a eventos es radicalmente diferente en concepto a la procedimental de toda la vida... ergo, tendremos que aprender a programar de nuevo.

Node se basa en el último motor de javascript + una librería de acceso de entrada salida asíncrona así que el concepto de páginas y carpetas se sustituye por otro parecido a servicios o puertas de entrada. Cada servicio de node será un servidor en sí escuchando en un websocket, listo para responder a los eventos que le lleguen.

El servicio usado para las pruebas de carga es éste, que es un respondedor automático que dice "Hello world" a todas las peticiones después de 2 segundos.

node.http.createServer(function (req, res) {
  setTimeout(function () {
    res.sendHeader(200, {"Content-Type": "text/plain"});
    res.sendBody("Hello World");
    res.finish();
  }, 2000);
}).listen(8000);
puts("Server running at http://127.0.0.1:8000/");

Lo que hace es escuchar el puerto 8000 y responde usando el protocolo http con el texto.

El truco es que al contrario que los servidores tradicionales, en esos 2 segundos (simulación del proceso que tendría que hacer), el servidor sigue aceptando petic... err... eventos y no bloque la ejecución.

Más de uno estará echando de menos librerías para acceso a base de datos (gracias PHP!) ya que aquí solo hay un esqueleto de aplicación sin demasiados añadidos aparte de leer y escribir.

Las buenas noticias son que hay múltiples librerías que utilizando el protocolo TCP/IP son capaces por ejemplo de atacar a servidores MySQL y ejecutar las operaciones necesarias para consultar, insertar, modificar, etc.

Otro ejemplo típico es un chat, que afortunadamente viene de "serie" como ejemplo con su código fuente aquí.

Para montar el servidor, necesitamos un hospedaje LINUX que permita instalarlo o un hospedaje gratuito de Node como por ejemplo Joyent. Para montarlo en casa, seguiremos necesitando linux y descargar los fuentes para luego compilarlos con
./configure
make
make install
La documentación, la podéis encontrar en esta dirección y la mailing list es ésta otra.

Para los que tengan curiosidad, se mantiene una lista de los módulos actualmente implementados en esta dirección que hacen ver que es un proyecto que está pero que muy vivo.

2011/07/06

Sprite Cow

Una de las mejores herramientas que he visto para la gestión de sprites es SpriteMe

Pero tener una buena herramienta de generación no sirve de nada si no queda completamente definido qué sprites son los que están en la imagen para incluirlos en el CSS. Aquí entra en juego SpriteCow, que de forma visual y muy sencilla permite seleccionar áreas de la imagen previamente cargada en un canvas, generándonos el CSS para acceder a los sprites.



Con ello, los pasos serían
  1. Generar la imagen del sprite. Ya sea a lo campestre con el photoshop o similar o con SpriteMe
  2. Cargar la imagen con SpriteCow y seleccionar los sprites que se deseen, copiando y pegando el CSS generado.
Rápido y sencillo, como debe ser.