2011/11/23

jQuery cssHook rotate3D

Los cssHooks de jQuery me parecen el no va más en simplificación y hacen un gran trabajo para hacer uniforme el comportamiento de los CSS a través de los distintos navegadores.

Para el que no sepa qué es un cssHook, le remito a la documentación de jQuery (http://api.jquery.com/jQuery.cssHooks/), pero en pocas palabras es la posibilidad de definir y usar propiedades CSS inventadas (o no) de modo que se puedan leer y ficjar sus valores, ejecutando un código concreto en este caso, claro.

Sirven para o crear nuevas propiedades CSS que hagan cosas maravillosas (este caso) o hacer compatibles distintos navegadores usando una misma propiedad CSS en todos ellos. P.e. con ellos se puede usar la propiedad border radius en IE6 y funcionará igual que en cualquier otro navegador.

Este cssHook, realiza una rotación isométrica de un elemento HTML. Se puede usar directamente mediante el método .css() de jQuery (tanto para hacer el set como para el get), así como parte de una llamada al método .animate() en todas sus variantes. El ángulo es en grados y se permiten tanto valores positivos como negativos así como expresiones de jQuery tipo '+=180deg', que lo que hará es sumarle 180 grados a la rotación actual.

Como un ejemplo es mejor que mil palabras, os pongo el jsfiddle del cssHook para que podáis probarlo en directo. Haced click en la carta para darle la vuelta. podía haber usado la imagen de una moneda, pero he preferido la de una carta.

Importante!
Dado que éste cssHook usa la propiedad CSS3 transform, para simplificar su uso sin tener que recurrir a transformaciones de matrices, usa a su vez otro cssHook que se encarga de hacer más sencilla la definición de la propiedad transform. En este caso, se ha usado el cssHook de brandonaaron/Louis Rémi pero hay otras alternativas aunque posiblemente, menos elegantes.

Extra!
Además del rotate3D, se define también otro cssHook llamado "flipped" para el control de cuándo se pasa de una cara a la otra al realizar la rotación.

Este cssHook, se encarga de mantener el estado de qué cara del elemento se está mostrando. Si la propiedad css 'flipped' es false, es la parte de adelante, si es false, la de atrás. El valor de la propiedad se cambia automáticamente cuando se gira el elemento al llegar a los 90 y a los 270 grados (módulo 360, como es lógico).

Asimismo, definiendo el método $.fn.flippedEvent, se consigue un callback en el momento que se produce el cambio de cara por ejemplo, para cambiar la imagen que se muestra.
$.fn.flippedEvent = function( elem, isFlipped ) { <yourCode> };

Y ahora, la versión minimizada de todo el código, que llega a unos ridículos 500bytes!!!
(function(b){if(!b.cssHooks)throw"jQuery 1.4.3 or above is required for this plugin to work";b.fn.flippedEvent=null;b.cssNumber.flipped=!0;b.cssHooks.flipped={get:function(a){return a.style.flipped||!1},set:function(a,d){var c=/true/i.test(d);"function"===typeof b.fn.flippedEvent&&b.fn.flippedEvent.flipEvent(a,c);a.style.flipped=c}};b.cssNumber.rotate3D=!0;b.cssHooks.rotate3D={get:function(a){return a.style.rotate3D},set:function(a,d){d=parseFloat(d);var c=d;if(c!=parseFloat(a.style.rotate3D)){var i=
0<=c?1:-1,f=Math.floor(Math.abs(c)),j=(180<=f%360?-1:1)*(1-f%180/90),g=b(a),e=parseInt(a.style.rotate3D),h=e<c?!0:!1,e=Math.abs(e)%180,c=Math.abs(parseInt(c))%180;(h&&90>e&&90<=c||!h&&90<e&&90>=c)&&g.css("flipped",!g.css("flipped"));g.css("transform","skew(0deg, "+i*f+"deg) scale("+j+", 1)")}a.style.rotate3D=d}};b.fx.step.rotate3D=function(a){b.cssHooks.rotate3D.set(a.elem,a.now,a.unit)}})(jQuery);

Código completo del cssHook. Como podréis ver, hace falta muy poco código para hacer que todo funcione.
/**
 * rotate3D/flipped jQuery cssHook v0.1
 * ==========================================================
 * (C) 2011 José Ramón Díaz - jrdiazweb@gmail.com
 *
 * Elegant and simple cssHooks to perform 3D-like css rotations and control 
 * visible side. 
 * 
 * REQUIRES.
 * A cssHook/code make transform CSS3 property work converting human readable 
 * values into its transformation matrix. In my case i've used the transform 
 * cssHooks from Louis-Rémi Babé to make it compatible, but you are free to use 
 * whatever you want meanwhile makes transform work.  
 *     https://github.com/brandonaaron/jquery-cssHooks
 *     https://github.com/louisremi/jquery.transform.js
 * 
 * CSS properties use the grammar:
 *     - "rotate3D: <n>[deg]"
 *     - "flipped: true|false"
 * 
 * When an element is rotated, flipped CSS property is set according the 
 * element orientation. Side changes occurs on 90/270 degrees.
 * 
 * Side change event callback can be used defining the function $.fn.flippedEvent. 
 *     $.fn.flippedEvent = function( elem, isFlipped ) { <yourcode> };
 * 
 * Examples:
 *     - elem.css('rotate3D')      - Gets current rotation
 *     - elem.css('rotate3D', 180) - Rotates element 180 degrees
 *     - elem.animate({ rotate3D, -180 }, 1000);    - Animates element from current rotation to -180 degrees
 *     - elem.animate({ rotate3D, "+=360" }, 1000); - Animates element adding 360 degrees to its current rotation
 *     - $('.c').animate({'rotate3D': '+=180'}, 2000, 'easeOutBounce') - Animates using easing function 'easeInOutBounce' defined in jQueryUI
 *
 * Browser copatibility.
 * Any modern CSS3 compatible browser.
 * 
 * NOTE: cssHooks needs jQuery v1.4.3 or greater.
 * 
 * Inspired by zachstronaut's rotate3Di
 *     http://www.zachstronaut.com/projects/rotate3di/
 * 
 * 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 ($) {
 // Check if cssHooks are supported
 if(!$.cssHooks) { throw( "jQuery 1.4.3 or above is required for this plugin to work" ); return; }

 ///////////////////////////////////////////////////////////////////////////
   // 'flipped' cssHook. Property has the format: "flipped: true|false"
   $.fn.flippedEvent = null; // Event function called when changing the side: "function( elem, isFlipped? ) {}"
 $.cssNumber['flipped'] = true; // Special units. None in this case
 $.cssHooks['flipped']  = {
  
  get      : function( elem, computed, extra ) { return elem.style['flipped'] || false; },
  set      : function( elem, value ) { 
      var val = /true/i.test(value); // Boolean conversion
      if( typeof $.fn.flippedEvent === "function" )
       $.fn.flippedEvent.flipEvent( elem, val );
      elem.style['flipped'] = val;  
     }
  
 };
        
 ///////////////////////////////////////////////////////////////////////////
 // 'rotate3D' csshook. Property has the format: "rotate3D: <n>deg|rad"
 $.cssNumber['rotate3D'] = true; // Special units support 'deg', 'rad', etc
 $.cssHooks['rotate3D']  = {
  
  get: function( elem, computed, extra ) { return elem.style['rotate3D']; },
  set: function( elem, value, unit ) {
      value = parseFloat( value );
      apply3DRotation( elem, value, unit );
      elem.style['rotate3D'] = value;
     }
        
 };

 // Animation step function for 'rotate3D' custom CSS property
 $.fx.step['rotate3D'] = function ( fx ) { $.cssHooks['rotate3D'].set( fx.elem, fx.now, fx.unit ); };

 // Performs 3D rotation on element
 var apply3DRotation = function( elem, value, unit ) {

  if( value == parseFloat( elem.style['rotate3D'] ) ) return; // No changes

  var dir   = value >= 0 ? 1 : -1;
  var deg   = Math.floor( Math.abs( value ) );
  var scale = ( ( deg % 360 ) >= 180 ? -1 : 1 ) * ( 1 - ( deg % 180 ) / 90 );

  var el  = $(elem); // Caches jQuery element to speed up execution

  var old = parseInt( elem.style['rotate3D'] );
  var inc = old < value ? true : false; 

  var before = Math.abs( old ) % 180;
  var after  = Math.abs( parseInt( value ) ) % 180;

  // Side change control
  if(  inc && before < 90  && after >= 90 || !inc && before > 90  && after <= 90 )
   el.css('flipped', !el.css('flipped') );

  // Performs CSS transform
  el.css( 'transform', 'skew(0deg, ' + dir * deg + 'deg) ' +
                 'scale(' + scale + ', 1)'
  );
 };

})(jQuery);

2011/11/15

Sesiones PHP a lo campestre III. No se puede hacer más lento.

Parafraseando al famoso mago (por si no lo conocéis es manco y hace trucos de cartas con una única mano), voy a darle la vuelta de tuerca definitiva a este método de obtener la información de sesión sin abrir la sesión.

Una vez que ya sabemos dónde están los ficheros de sesión tal y como se ha visto en las entregas anteriores, podemos simular de forma idéntica a como hace PHP la carga de la información de la sesión en una matriz asociativa.

El primer paso es cargar los contenidos del fichero de la sesión.
$fname    = session_save_path() . "/sess_" . $sid;
$_session = unserializeSession( file_get_contents( $fname ) );

Y para ello, se usa la siguiente función. Si es que es demasiado fácil...
///////////////////////////////////////////////////////////////////////////////
/**
 * unserializeSession - Unserialize para ficheros de sesión
 *
 * Convierte una cadena que se corresponde con los contenidos de un fichero de sesión en un array 
 * asociativo con las claves y valores de la sesión correspondiente.
 *
 * @access    public
 * @link      http://www.php.net/manual/en/function.session-decode.php#101687
 *
 * @param     string  $data  Cadena de texto del fichero de la sesión
 * @return    array          Array asociativa con las claves y valores
 */
function unserializeSession( &$data )
{
       if(  strlen( $data) == 0)
       {
              return array();
       }

       // match all the session keys and offsets
       preg_match_all('/(^|;|\})([a-zA-Z0-9_]+)\|/i', $data, $matchesarray, PREG_OFFSET_CAPTURE);

       $returnArray = array();

       $lastOffset = null;
       $currentKey = '';
       foreach ( $matchesarray[2] as $value )
       {
              $offset = $value[1];
              if(!is_null( $lastOffset))
              {
                     $valueText = substr($data, $lastOffset, $offset - $lastOffset );
                     $returnArray[$currentKey] = unserialize($valueText);
              }
              $currentKey = $value[0];

              $lastOffset = $offset + strlen( $currentKey )+1;
       }

       $valueText = substr($data, $lastOffset );
       $returnArray[$currentKey] = unserialize($valueText);

       return $returnArray;
}

2011/11/14

Colocando el cursor con javascript

Seguro que alguna vez hemos querido colocar el cursor de un cuadro de texto o un textarea para cualquier cosa que se nos haya ocurrido, la pega es que no hay métodos nativos para ello, pero jugando un poco con los rangos de selección, es fácil de hacer.

A continuación os pongo dos pequeñas funciones para conseguir el setter/getter de la posición del cursor. Es una "tontá", pero es efectiva. Además si lo minimizas ocupa una miseria.

function getCursorPosition( elem ) {

    var CaretPos = 0;
    // IE Support
    if( document.selection ) {

        elem.focus();
        var Sel = document.selection.createRange();

        Sel.moveStart( 'character', -elem.value.length );

        CaretPos = Sel.text.length;
    }
    // Firefox support
    else if ( elem.selectionStart || elem.selectionStart == '0' )
        CaretPos = elem.selectionStart;

    return( CaretPos );
}


function setCursorPosition( elem, pos )
{
    if( elem.setSelectionRange )
    {
        elem.focus();
        elem.setSelectionRange( pos,pos );
    }
    else if( elem.createTextRange ) {
        var range = elem.createTextRange();
        range.collapse( true );
        range.moveEnd( 'character', pos );
        range.moveStart( 'character', pos );
        range.select();
    }
}

2011/10/27

jQuery Canvas Minimap

Y para rematar la semana, la joya de la corona. Otro plugin hecho por mí que toma un contenedor y genera un minimapa de éste y que pese a basarse en otro ya existente llamado Fracs, le da un giro de tuerca y lo hace más flexible.

El principio de este plugin es tomar un contenedor del que se quiere hacer un minimapa e indicando un canvas de destino, pinta sobre él los elementos cuyos selectores hayamos definido usando el estilo que deseemos.

Usarlo es lo más sencillo del mundo. Dados un canvas #myCanvas y un contenedor
var minimapInstance = $('#myCanvas').minimap( $('myContainer') [, options] );

Las opciones son opcionales, pero recomendables pues es donde se definen qué elementos se representarán en el minimapa y cómo se hará. Concretamente, la encargada de ello es la propiedad "style" que  es un array de objetos con la siguiente estructura:
styleDef {
       // selector to use for that style
       selector: String / HTMLElement / [HTMLElement, ...] / jQuery

       // border stroke width
       strokeWidth: float / undefined / 'auto'

       // border color
       strokeStyle: String / undefined / 'auto'

       // fill color
       fillStyle: String / undefined / 'auto'
 }

Como una imagen vale más que mil palabras, como es usual, el repositorio del código está en mi jsFiddle. Solo hace falta el código javascript.

El código completo es éste
/**
 * jquery.minimap-0.1.js - Container Minimap Plugin
 * ==========================================================
 * (C) 2011 José Ramón Díaz - jrdiazweb@gmail.com
 *
 * http://3nibbles.blogspot.com/jquery-canvas-minimap.html
 * http://plugins.jquery.com/project/CanvasMinimap
 *
 * Container Minimap is a plugin that creates a minimap of the desired container
 * scaled down and styled so you can highlight the overall position of the
 * contents of the container element.
 *
 * Scale is calculated based on the minumum of width or height of the canvas used
 * to draw the minimap. So, using only CSS or attributes you can modify the scale
 * adjusting it to the desired size of the container.
 *
 * INSTANTIATION
 * Call the minimap method over the selector of the canvas where the minimap is
 * going to be drawn
 *
 *     minimapInstance = $('#myCanvas').minimap( $('myContainer') [, options] );
 *
 * The only HTML needed is the target canvas and the container to be drawn.
 *
 * OPTIONS
 *     - container : document.    Defaults to whole page. Otherwise is a
 *                                selector of the desired container to be mapped.
 *     - styles    : [ styleDef, ... ]    array with the styles to apply.
 *     - viewportStyle : styleDef. Style of the viewport.
 *     - viewportDragStyle: styleDef. Style of the viewport
 *
 * The OutlineStyle style must be in the following format (Same format than Fracs):
 *
 *     styleDef {
 *         // selector to use for that style
 *         selector: String / HTMLElement / [HTMLElement, ...] / jQuery
 *
 *         // border stroke width
 *         strokeWidth: float / undefined / 'auto'
 *
 *         // border color
 *         strokeStyle: String / undefined / 'auto'
 *
 *         // fill color
 *         fillStyle: String / undefined / 'auto'
 *     }
 *
 * strokeWidth, strokeColor, fillColor may have the special values
 * undefined to ignore and 'auto' to fetch values from css.
 *
 * PUBLIC API
 *     - $.minimap.redraw()    Redraws the minimap
 *     - drag                  Boolean that indicates that the viewport is being dragged
 *
 * Legal stuff
 *     Based on jQuery plugin named Fracs, but modified to allow minimapping of a
 *     desired container and some other enhancements.
 *
 *     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( $ ) {

 var $window   = $(window)
   , $document = $(document)
   ;

 ////////////////////////////////////////////////////////////////////////////////
    Rect = function (left, top, width, height) {

        if (!(this instanceof Rect)) {
            return new Rect(left, top, width, height);
        }

        this.left   = Math.round( left   );
        this.top    = Math.round( top    );
        this.width  = Math.round( width  );
        this.height = Math.round( height );
        this.right  = this.left + this.width;
        this.bottom = this.top  + this.height;
    };

    Rect.prototype = {
        equals: function( that ) {
            return this.left === that.left && this.top === that.top && this.width === that.width && this.height === that.height;
        },

        area: function() {
            return this.width * this.height;
        },

        intersection: function( rect ) {
            var left   = Math.max( this.left  , rect.left   ),
                right  = Math.min( this.right , rect.right  ),
                top    = Math.max( this.top   , rect.top    ),
                bottom = Math.min( this.bottom, rect.bottom ),
                width  = right  - left,
                height = bottom - top;

            return ( width >= 0 && height >= 0 ) ? Rect( left, top, width, height ) : undefined;
        },

        envelope: function( rect ) {
            var left   = Math.min( this.left  , rect.left   ),
                right  = Math.max( this.right , rect.right  ),
                top    = Math.min( this.top   , rect.top    ),
                bottom = Math.max( this.bottom, rect.bottom ),
                width  = right  - left,
                height = bottom - top;

            return Rect( left, top, width, height );
        }
    };


    /**
     * Special constructors
     */
    Rect.ofDocument = function() {
        return Rect( 0, 0, $document.width(), $document.height() );
    };

    Rect.ofViewport = function() {
        return Rect( $window.scrollLeft(), $window.scrollTop(), $window.width(), $window.height() );
    };

    Rect.ofElement = function( element ) {
        var $element = $(element)
          , offset;

        if( !$element.is(":visible") ) {
            return Rect( 0, 0, -1, 0 );
        }

        offset = $element.offset();
        return Rect( offset.left, offset.top, $element.outerWidth(), $element.outerHeight() );
    };

    Rect.ofElementOS = function( element, contOffset ) {
        var $element = $(element)
          , offset;

        if( typeof contOffset === 'undefined' || !contOffset ) contOffset = { left: 0, top: 0 };
        if( !$element.is(":visible") ) {
            return Rect( 0, 0, -1, 0 );
        }

        offset = $element.offset();
        return Rect( offset.left - contOffset.left, offset.top - contOffset.top, $element.outerWidth(), $element.outerHeight() );
    };


 ////////////////////////////////////////////////////////////////////////////////
    // Instantiation
    $.fn.minimap = function( container, options ) {

     var contOffset;

     // Private members
     function MiniMap( canvas, container, options ) {

         if ( !(this instanceof MiniMap) ) {
             return new MiniMap( canvas, container, options );
         }

         if ( !canvas.nodeName || canvas.nodeName.toLowerCase() !== "canvas" ) {
             return undefined;
         }

         var me = this
           , $canvas    = $(canvas)
           , $container = $(container) || document
           , width      = $canvas.width()  || $canvas.attr("width")
           , height     = $canvas.height() || $canvas.attr("height")
           , context    = canvas.getContext("2d")
           , drag       = false
           , vpRect
           , scale
           ;

         options = $.extend( {}, $.fn.minimap.defaults, options );

      // ========================================================================
      // Public Members
         this.redraw = function() {
          // Gets the container offset to transform contents position
          contOffset = $(container).offset();
          if( !contOffset ) contOffset = { left: 0, top: 0 };

          // Gets the Rect of the container
          docRec = null;
    if( $container === document )
     docRect = Rect.ofDocument();
    else
     docRect = Rect.ofElement( $container );
    vpRect = Rect.ofViewport();
    scale = Math.min(width / docRect.width, height / docRect.height);

    // Scales canvas on size and on inner transform
    $canvas.height( docRect.height * scale );
    $canvas.width(  docRect.width  * scale );

    context.setTransform( 1, 0, 0, 1, 0, 0 );
    context.clearRect( 0, 0, $canvas.width(), $canvas.height() );

    //context.scale( scale, scale ); // WRONG! deforms the projection
    context.scale( width / docRect.width, height / docRect.height );

    // Draws the minimap
    applyStyles();
    //drawViewport();
         },

         drawRect = function( rect, strokeWidth, strokeStyle, fillStyle, invert ) {
          // Draws a Rect on canvas
             if ( strokeStyle || fillStyle ) {
                 if (fillStyle) {
                     context.beginPath();
                     if (invert) {
                         context.rect( 0, 0, docRect.width, rect.top );
                         context.rect( 0, rect.top, rect.left, rect.height );
                         context.rect( rect.right, rect.top, docRect.right - rect.right, rect.height );
                         context.rect( 0, rect.bottom, docRect.width, docRect.bottom - rect.bottom );
                     } else {
                         context.rect( rect.left, rect.top, rect.width, rect.height );
                     }
                     context.fillStyle = fillStyle;
                     context.fill();
                 }
                 if (strokeStyle) {
                     context.beginPath();
                     context.rect( rect.left, rect.top, rect.width, rect.height );
                     context.lineWidth = scale ? Math.max( strokeWidth, 0.2 / scale ) : strokeWidth;
                     context.strokeStyle = strokeStyle;
                     context.stroke();
                 }
             }
         },

         drawElement = function( element, strokeWidth, strokeStyle, fillStyle ) {
          // Gets the Rect of an element and draws it on canvas
             var $element = $(element),
                 rect = Rect.ofElementOS( element, contOffset );
              //rect = Rect.ofElement( element );

             if ($element.css("visibility") === "hidden" || rect.width === 0 || rect.height === 0) {
                 return;
             }

             strokeWidth = strokeWidth === "auto" ? parseInt($element.css("border-top-width"), 10) : strokeWidth;
             strokeStyle = strokeStyle === "auto" ? $element.css("border-top-color") : strokeStyle;
             fillStyle   = fillStyle   === "auto" ? $element.css("background-color") : fillStyle;
             drawRect(rect, strokeWidth, strokeStyle, fillStyle);
         },

         applyStyles = function () {
          // Loops through the contents of the container
             $.each( options.styles, function (idx, style) {
                 $(style.selector).each(function () {
                     drawElement( this, style.strokeWidth, style.strokeStyle, style.fillStyle );
                 });
             });
         },

         drawViewport = function () {
             var style = drag && options.viewportDragStyle ? options.viewportDragStyle : options.viewportStyle;

             drawRect( vpRect, style.strokeWidth, style.strokeStyle, style.fillStyle, options.invertViewport );
         };

         me.redraw();
     }

        return new MiniMap( this.get(0), container, options );
    };

    // ========================================================================
    // Minimap options defaults
    $.fn.minimap.defaults = {
        container  : document,        // Defaults to whole page. Otherwise is a selector of the container
        styles     : [{               // Sample styles
                      selector: "header,footer,section,article",
                      fillStyle: "rgb(230,230,230)"
                     }, {
                      selector: "h1",
                      fillStyle: "rgb(240,140,060)"
                     }, {
                      selector: "h2",
                      fillStyle: "rgb(200,100,100)"
                     }, {
                      selector: "h3",
                      fillStyle: "rgb(100,200,100)"
                     }, {
                      selector: "h4",
                      fillStyle: "rgb(100,100,200)"
                     }]
    };

}(jQuery));

Y el código minimizado que llega a ocupar menos de 1.5Kb es
/** !jquery.minimap-0.1.js - Container Minimap Plugin
 * (C) 2011 José Ramón Díaz - jrdiazweb@gmail.com
 * http://3nibbles.blogspot.com/jquery-canvas-minimap.html | http://plugins.jquery.com/project/CanvasMinimap
 *
 * Based on jQuery plugin named Fracs, but modified to allow minimapping of a desired container and some other enhancements.
 * You are free to use this code, but you must give credit and/or keep header intact.
 */
(function(f){var h=f(window),j=f(document);Rect=function(a,b,c,e){if(!(this instanceof Rect))return new Rect(a,b,c,e);this.left=Math.round(a);this.top=Math.round(b);this.width=Math.round(c);this.height=Math.round(e);this.right=this.left+this.width;this.bottom=this.top+this.height};Rect.prototype={equals:function(a){return this.left===a.left&&this.top===a.top&&this.width===a.width&&this.height===a.height},area:function(){return this.width*this.height},intersection:function(a){var b=Math.max(this.left,
a.left),c=Math.min(this.right,a.right),e=Math.max(this.top,a.top),a=Math.min(this.bottom,a.bottom);c-=b;a-=e;return c>=0&&a>=0?Rect(b,e,c,a):void 0},envelope:function(a){var b=Math.min(this.left,a.left),c=Math.max(this.right,a.right),e=Math.min(this.top,a.top),a=Math.max(this.bottom,a.bottom);return Rect(b,e,c-b,a-e)}};Rect.ofDocument=function(){return Rect(0,0,j.width(),j.height())};Rect.ofViewport=function(){return Rect(h.scrollLeft(),h.scrollTop(),h.width(),h.height())};Rect.ofElement=function(a){var a=
f(a),b;if(!a.is(":visible"))return Rect(0,0,-1,0);b=a.offset();return Rect(b.left,b.top,a.outerWidth(),a.outerHeight())};Rect.ofElementOS=function(a,b){var c=f(a),e;if(typeof b==="undefined"||!b)b={left:0,top:0};if(!c.is(":visible"))return Rect(0,0,-1,0);e=c.offset();return Rect(e.left-b.left,e.top-b.top,c.outerWidth(),c.outerHeight())};f.fn.minimap=function(a,b){function c(a,b,i){if(!(this instanceof c))return new c(a,b,i);if(a.nodeName&&a.nodeName.toLowerCase()==="canvas"){var g=f(a),h=f(b)||document,
j=g.width()||g.attr("width"),l=g.height()||g.attr("height"),d=a.getContext("2d"),m,k,i=f.extend({},f.fn.minimap.defaults,i);this.redraw=function(){(e=f(b).offset())||(e={left:0,top:0});docRec=null;docRect=h===document?Rect.ofDocument():Rect.ofElement(h);m=Rect.ofViewport();k=Math.min(j/docRect.width,l/docRect.height);g.height(docRect.height*k);g.width(docRect.width*k);d.setTransform(1,0,0,1,0,0);d.clearRect(0,0,g.width(),g.height());d.scale(j/docRect.width,l/docRect.height);applyStyles()};drawRect=
function(a,b,c,e,f){if(c||e){if(e)d.beginPath(),f?(d.rect(0,0,docRect.width,a.top),d.rect(0,a.top,a.left,a.height),d.rect(a.right,a.top,docRect.right-a.right,a.height),d.rect(0,a.bottom,docRect.width,docRect.bottom-a.bottom)):d.rect(a.left,a.top,a.width,a.height),d.fillStyle=e,d.fill();if(c)d.beginPath(),d.rect(a.left,a.top,a.width,a.height),d.lineWidth=k?Math.max(b,0.2/k):b,d.strokeStyle=c,d.stroke()}};drawElement=function(a,b,c,d){var g=f(a),a=Rect.ofElementOS(a,e);g.css("visibility")==="hidden"||
a.width===0||a.height===0||(b=b==="auto"?parseInt(g.css("border-top-width"),10):b,c=c==="auto"?g.css("border-top-color"):c,d=d==="auto"?g.css("background-color"):d,drawRect(a,b,c,d))};applyStyles=function(){f.each(i.styles,function(a,b){f(b.selector).each(function(){drawElement(this,b.strokeWidth,b.strokeStyle,b.fillStyle)})})};drawViewport=function(){var a=i.viewportStyle;drawRect(m,a.strokeWidth,a.strokeStyle,a.fillStyle,i.invertViewport)};this.redraw()}}var e;return new c(this.get(0),a,b)};f.fn.minimap.defaults=
{container:document,styles:[{selector:"header,footer,section,article",fillStyle:"rgb(230,230,230)"},{selector:"h1",fillStyle:"rgb(240,140,060)"},{selector:"h2",fillStyle:"rgb(200,100,100)"},{selector:"h3",fillStyle:"rgb(100,200,100)"},{selector:"h4",fillStyle:"rgb(100,100,200)"}]}})(jQuery);

Éste ha quedado chulo, ein?

No significa nada, pero me hace ilu!

Mi plugin de WizardSteps está el número 3 de la lista de los 10 plugins de navegación más creativos según script-tutorials.com

http://www.script-tutorials.com/10-most-creative-jquery-navigation-plugins/

Y también se me nombra en otros sitios en posts con el apelativo de "superb" :D

http://www.multyshades.com/2011/08/30-superb-jquery-plugins-for-dropdown-navigation-menus/

Ya se que no es un ranking mundial ni nada de eso, pero siempre es agradable algo de reconocimiento, no?

Detectando si un contenedor tiene barras de scroll

Hola, hoy toca algo un poco más ligero que la entrada de ayer y os comparto otro mini-plugin de jQuery para detectar si un elemento tiene barras de scroll o no. No es mío, pero lo importante es que es diminuto y funciona a la perfección.

/* Plugin to test if the element has vertical scrollbar or not */
(function($) {
 $.fn.hasScrollBar = function() { return this.get(0).scrollHeight > this.height(); }
})(jQuery);

Para invocarlo, nada más sencillo que llamarlo sobre el selector del elemento a comprobar y devuelve tru o false.

Por ejemplo si tenemos un div llamado #miDiv, sería:  $('#miDiv').hasScrollBar();

Sencillo, no?, pues preparaos, que en breve viene el próximo macro-plugin jQuery by me (o al menos, mayormente jejejejeje).

2011/10/26

Plugin jQuery Sliding Panel

Hola, hoy os presento uno de los plugins de jQuery que he hecho y que os he prometido para esta semana. Hablamos de la continuación del Slide Button ya que comparte gran parte de la apariencia y funcionalidad, pero aplicado a paneles en vez de a botones.


Este plugin está basado en uno denominado jqEasy (un saludo!), pero con cuyas tripas no estoy de acuerdo, ya que arrastraba varios fallos, no seguía las recomendaciones de desarrollo de plugins, definiciones CSS incompletas y memory leaks graves, así que opté por reescribirlo todo desde cero, tanto el CSS como javascript adaptándolo a la apariencia de los slide buttons (lo cual no quiere decir que mi código sea infalible, solo que los errores que detecté han sido corregidos).

Usarlo es lo más sencillo del mundo, ya que solo hay que llamar a la función slidePanel() sobre el contenedor del widget, que tiene que tener una mínima estructura que os detallaré más adelante. La única particularidad es que si se quiere a la izquierda no hay que hacer nada, pero si se quiere a la derecha hay que anadir la clase ".right" al contendor (o usar las propiedades del plugin).

Por defecto también considera que está "fixed" a la página, pero cambiando la propiedad position del CSS (o del objeto jQuery) a "relative" o "absolute", se cambia el comportamiento del widget radicalmete. Para más detalles podéis comprobar el ejemplo del jsFiddle, donde hay ejemplos con las 6 combinaciones posibles.

También es fácilmente integrable con jQueryUI (los estilos aún no, lo siento), pudiéndose usar dentro de una pila de llamadas para añadirle comportamiento con draggable() o modificando las funciones de despliegue del panel mediante las propiedades del objeto jQuery.

Debe funcionar en todos los navegadores, incluido el IE6 (aunque no soporta la propiedad CSS "position: fixed" sustituyéndose por "position: absolute" de forma automática). Evidentemente, mientras más "moderno" sea el navegador, más bonito se verá (transparencias, esquinas redondeadas, etc), pero eso es cosa de CSS y no de código.

Sin más demoras, os dejo el jsF


iddle, donde está todo el código, css y ejemplos necesarios. El código está comentado así como documentado así que no deberíais tener mucho problema. Si encontrarais un bug, no dudéis en comunicármelo. Alternativamente, podéis visitar la página del plugin en la página oficial de jQuery.


Y para los más impacientes, el código completo:

Estructura básica HTML
<div class="slidePanelWidget" id="widget1">
    <a class="slidePanelButton" href="">&nbsp;Widget 1</a>
    <div class="slidePanel panel">

        These are the contents of the widget!
    </div>
</div>

Código javascript minimizado. Llega a ocupar un mínimo de unos 900 bytes. Quién pide más?
/*! jquery.slidePanel-0.1.js - Slide Panel plugin for jQuery
* ==========================================================
* (C) 2011 José Ramón Díaz - jrdiazweb@gmail.com - v.0.1.0 (25/10/2010)
* Requires: jQuery v1.4.3+
* http://3nibbles.blogspot.com/2011/10/plugin-jquery-sliding-panel.html
* 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(b){function f(c,d){var g=b.browser.msie&&b.browser.version=="6.0",a=this;a.opts=d;a.widget=c;a.button=a.widget.find(".slidePanelButton");a.panel=a.widget.find(".slidePanel");a.left=a.widget.hasClass("right")?false:true;if(g)a.opts.position=a.opts.position=="fixed"?"absolute":a.opts.position;a.opts.position&&a.button.css("position",a.opts.position);a.opts.buttonTop&&a.button.css("top",a.opts.buttonTop);a.opts.panelTop&&a.panel.css("top",a.opts.panelTop);a.button.attr("href","javascript:void(0)").click(function(){a.opts.ajax?
a.panel.is(":visible")?a.toggle():a.panel.load(a.opts.ajax,function(b,c){c!=="success"&&a.panel.html("Sorry, but there was an error loading the document.

");a.toggle()}):a.toggle();return false});a.deployed=false;a.toggle=function(b){if(typeof open==="boolean")a.deployed=b;a.deployed?a.close():a.open()};var e=0;a.open=function(){a.deployed=true;var b=a.button.width();b&&(oldPaddingR=a.button.css("padding-right"),oldPaddingL=a.button.css("padding-left"),e=b);a.button.addClass("deployed");a.left?
a.button.animate({width:"0px","padding-right":"0px"},a.opts.speed,a.opts.animFunction):a.button.animate({width:"0px","padding-right":"0px","padding-left":oldPaddingR},a.opts.speed,a.opts.animFunction);a.panel.toggle(a.opts.speed,a.opts.animFunction,function(){});a.opts.clickToClose&&a.enableCloseFn()};a.close=function(){a.deployed=false;a.panel.toggle(a.opts.speed,a.opts.animFunction,function(){});a.left?a.button.animate({width:e+"px","padding-right":oldPaddingR},a.opts.speed,a.opts.animFunction):
a.button.animate({width:e+"px","padding-right":oldPaddingR,"padding-left":oldPaddingL},a.opts.speed,a.opts.animFunction);a.button.removeClass("deployed");a.opts.clickToClose&&a.disableCloseFn()};a.stopCloseFn=function(a){a.stopPropagation()};a.enableCloseFn=function(){b(document).bind("click",a.close);a.panel.bind("click",a.stopCloseFn)};a.disableCloseFn=function(){b(document).unbind("click",a.close);a.panel.unbind("click",a.stopCloseFn)}}b.fn.slidePanel=function(c){var d=b.extend({},{position:null,
buttonTop:null,panelTop:null,animFunction:"swing",speed:"fast",ajax:null,clickToClose:true},c);return!this.data("slidePanel")||typeof c=="object"?this.each(function(){var c=b(this);c.data("slidePanel",new f(c,d))}):this.data("slidePanel")}})(jQuery);

Código javascript completo
/*! jquery.slidePanel-0.1.js - Slide Panel plugin for jQuery
 * ==========================================================
 * (C) 2011 José Ramón Díaz - jrdiazweb@gmail.com
 * Version: 0.1.0 (25/10/2010)
 * Requires: jQuery v1.4.3+
 *
 * http://3nibbles.blogspot.com/2011/10/plugin-jquery-sliding-panel.html
 *
 * INSTANTIATION
 * The basic structure is a div indicating on his class if it must be at
 * left (default, no extra class needed) or right (".right" class needed).
 * Inside the div there must be an <a class="slidePanelButton"> and a
 * <div class="slidePanel">. That's all.
 *
 * Manual instantiation
 *     $('.slidePanelWidget').slidePanel( [ options_object ] );
 *
 * Optionally if you use jQueryUI, you can add draggable to it with
 *     $('.slidePanelWidget').slidePanel( [ options_object ] ).draggable( { axis: 'y' } );
 *
 * OPTIONS
 *     - position     : null null (CSS position) / 'fixed' / 'absolute'
 *     - buttonTop    : null top position of the button inside slidePanelWidget. Null = CSS specified.
 *     - panelTop     : null top position of the panel inside slidePanelWidget. Null = CSS specified.
 *     - animFunction : 'swing' Animation function used to deploy panel
 *     - speed        : 'fast' Animation speed
 *     - ajax         : null URL of th AJAX call to perform to load panel contents. It is loaded before deploying the panel.
 *     - clickToClose : true True = click anywhere to close the panel
 *
 * API
 *     - toggle()  Toggles open/close panel state
 *     - open()    Forces open panel
 *     - close()   Forces close panel
 *     - deployed  Indicates that the control is deployed (opened)
 *
 * CLASSES USED
 *     - Container: .slidePanelWidget, .right
 *     - Button:    .slidePanelButton, .deployed
 *     - Content:   .slidePanel
 *
 * HTML format
 *     <div id="widget1" class="slidePanelWidget">
 *        <a class="slidePanelButton"> Name</a>
 *        <div class="slidePanel panel">
 *
 *            <!-- Widget contents -->
 *            Paste here the contents of the panel
 *            <!-- End of widget contents -->
 *
 *        </div>
 *      </div>
 *
 * 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.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 *
 * Modifications:
 *     - 2011-10-25  JRDiaz  Creation
 */
(function($){

    // Instantiation
 $.fn.slidePanel = function( options ) {
  var defaults = {
     position     : null
   , buttonTop    : null
   , panelTop     : null
   , animFunction : 'swing'
   , speed        : 'fast'
   , ajax         : null
   , clickToClose : true
  };

  var opts = $.extend( {}, defaults, options );

  if( !this.data('slidePanel') || typeof options == 'object' )
  {
   // Not instantiated or part of a selector call
   return this.each(function() {
    var me = $(this);
    me.data('slidePanel', new SlidePanel( me, opts ));
   });
  }
  else
  {
   return this.data('slidePanel');
  }
    };

 // SlidePanel Object
    function SlidePanel( el, opts ) {
  var isIE6  = $.browser.msie && $.browser.version == "6.0";

  var me    = this;
  me.opts   = opts;
  me.widget = el;
  me.button = me.widget.find('.slidePanelButton');
  me.panel  = me.widget.find('.slidePanel');
  me.left   = me.widget.hasClass('right') ? false : true;

  // ie6 doesn't like fixed position
  if( isIE6 ) { me.opts.position = me.opts.position == 'fixed' ? 'absolute' : me.opts.position; }

  // set css properties for button and panel
  if( me.opts.position  ) me.button.css( 'position', me.opts.position );
  if( me.opts.buttonTop ) me.button.css( 'top', me.opts.buttonTop );
  if( me.opts.panelTop  ) me.panel.css ( 'top', me.opts.panelTop  );

  // Button click event
  //me.button.attr( "href", "javascript:void(0)" ).mousedown(function() {
  me.button.attr( "href", "javascript:void(0)" ).click( function() {
   // load default content if ajax is false
   if (!me.opts.ajax) {
    //me.panel.toggle( me.opts.speed, me.opts.animFunction, function () {} );
    me.toggle();
   }
   else
   {
    // Loads the panel contents via AJAX if opts.ajax is defined
    if ( !me.panel.is(':visible') ) { // fetch data ONLY when panel is hidden...
     me.panel.load(me.opts.ajax, function( response, status, xhr ) {
      if (status !== "success") { // the ajax source wasn't loaded properly
       var msg = "<p>Sorry, but there was an error loading the document.</p>";
       me.panel.html(msg);
      };
      // Sets the HTML of the panel BEFORE opening it
      //me.panel.toggle( me.opts.speed, me.opts.animFunction, function () {} );
      me.toggle();
     });
    } else {
     //me.panel.toggle( me.opts.speed, me.opts.animFunction, function () {} );
     me.toggle();
    }
   }
   //me.button.toggleClass("deployed");
   return false;
  });

  // Control functions
  me.deployed = false;
  me.toggle = function( deployPanel ) {
   if( typeof open === 'boolean' ) me.deployed = deployPanel; // Forces open or close
   if( !me.deployed ) me.open();
   else               me.close();
  };

  var oldPadding = '0px';
  var oldWidth   = 0;
  me.open = function() {
   me.deployed = true;

   // Stores old values
   var w = me.button.width();
   if( w ) {
    oldPaddingR = me.button.css('padding-right');
    oldPaddingL = me.button.css('padding-left');
    oldWidth   = w;
   }
   me.button.addClass( 'deployed' );
   // Animates button
   if( me.left )
    me.button.animate( { 'width': '0px', 'padding-right': '0px' }, me.opts.speed, me.opts.animFunction );
   else
    me.button.animate( { 'width': '0px', 'padding-right': '0px', 'padding-left': oldPaddingR }, me.opts.speed, me.opts.animFunction );
   me.panel.toggle( me.opts.speed, me.opts.animFunction, function () {} );

   if ( me.opts.clickToClose ) me.enableCloseFn(); // Enables autoclose function if clickToClose is enabled
  };

  me.close = function() {
   me.deployed = false;

   me.panel.toggle( me.opts.speed, me.opts.animFunction, function () {} );
   // Animates button
   if( me.left )
    me.button.animate( { 'width': oldWidth + 'px', 'padding-right': oldPaddingR }, me.opts.speed, me.opts.animFunction );
   else
    me.button.animate( { 'width': oldWidth + 'px', 'padding-right' : oldPaddingR, 'padding-left': oldPaddingL }, me.opts.speed, me.opts.animFunction );
   me.button.removeClass( 'deployed' );

   if ( me.opts.clickToClose ) me.disableCloseFn(); // Disables autoclose function if clickToClose is enabled
  };

  // Autoclose panel when clicking outside
  me.stopCloseFn    = function(e) { e.stopPropagation(); }; // don't close panel when clicking inside it
        me.enableCloseFn  = function( ) { $(document).bind  ( 'click', me.close ); me.panel.bind  ('click', me.stopCloseFn ); };
        me.disableCloseFn = function( ) { $(document).unbind( 'click', me.close ); me.panel.unbind('click', me.stopCloseFn ); }; // Removes binds
 };
})(jQuery);

Código CSS
/*
 * jQuery slidePanel plugin basic styles
 * Version: 0.1.0 (2011/10/25)
 */

/* Widget styles */
.slidePanelWidget { position: fixed; .position: absolute; left: 0; }
.slidePanelWidget.right { left: auto; right: 0; }

  /* Button styles */
  .slidePanelWidget a.slidePanelButton:focus { outline: none; }

  .slidePanelWidget .slidePanelButton {
    white-space: nowrap;
    overflow: hidden;
    position: absolute;
    top: 4px;
    height: 19px;
    background-color: #BAC8DC;

    text-decoration: none;
    color: #fff;
    font-weight: bold;

    z-index: 2;
  }

  /* Left/right button styles */
  .slidePanelWidget .slidePanelButton {
   left: 0;
    padding: 1px 10px 1px 20px;
    background-position: 0 0;

           -moz-border-radius-topright: 10px;
        -khtml-border-top-right-radius: 10px;
       -webkit-border-top-right-radius: 10px;
               border-top-right-radius: 10px;

        -moz-border-radius-bottomright: 10px;
     -khtml-border-bottom-right-radius: 10px;
    -webkit-border-bottom-right-radius: 10px;
            border-bottom-right-radius: 10px;
  }
  .slidePanelWidget.right .slidePanelButton {
   left: auto; right: 0;
    padding: 1px 20px 1px 10px;
    background-position: 100% 0;

                   -moz-border-radius: 0;
                 -khtml-border-radius: 0;
                -webkit-border-radius: 0;
                        border-radius: 0;

           -moz-border-radius-topleft: 10px;
        -khtml-border-top-left-radius: 10px;
       -webkit-border-top-left-radius: 10px;
               border-top-left-radius: 10px;

        -moz-border-radius-bottomleft: 10px;
    -khtml-border-bottom-right-radius: 10px;
    -webkit-border-bottom-left-radius: 10px;
            border-bottom-left-radius: 10px;
  }

  /* Dynamic change of sprites. Sprites are 19px tall */
  .slidePanelWidget .slidePanelButton         { /* Open sprite */ background: #BAC8DC url(data:image/png;base64,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) no-repeat 0 0; }
    .slidePanelWidget .slidePanelButton:hover { background-color: #59B; background-position: 0 -19px; }
    .slidePanelWidget.right .slidePanelButton:hover { background-position: 100% -19px; }

  .slidePanelWidget .slidePanelButton.deployed         { /* Closed sprite */ background: #666 url(data:image/png;base64,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) no-repeat 0 0; }
    .slidePanelWidget .slidePanelButton.deployed:hover { background-color: #59B; background-position: 0 -19px; }
    .slidePanelWidget.right .slidePanelButton.deployed:hover { background-position: 0 -19px; }


  /* Panel styles */
  .slidePanelWidget .slidePanel {
    display: none;
    position: absolute;
    width: auto;
    height: auto;
    z-index: 1;

    padding: 8px;
    color:#CCC;
    background: #111;

      -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=90)";
          filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=90);
    -moz-opacity: .9;
         opacity: .9;
  }

  /* Left/right panel styles */
  .slidePanelWidget .slidePanel {
    left: 0;
           -moz-border-radius-topright: 4px;
        -khtml-border-top-right-radius: 4px;
       -webkit-border-top-right-radius: 4px;
               border-top-right-radius: 4px;

        -moz-border-radius-bottomright: 4px;
     -khtml-border-bottom-right-radius: 4px;
    -webkit-border-bottom-right-radius: 4px;
            border-bottom-right-radius: 4px;
  }
  .slidePanelWidget.right .slidePanel {
    left: auto;
    right: 0;

                   -moz-border-radius: 0;
                 -khtml-border-radius: 0;
                -webkit-border-radius: 0;
                        border-radius: 0;

           -moz-border-radius-topleft: 4px;
        -khtml-border-top-left-radius: 4px;
       -webkit-border-top-left-radius: 4px;
               border-top-left-radius: 4px;

        -moz-border-radius-bottomleft: 4px;
     -khtml-border-bottom-left-radius: 4px;
    -webkit-border-bottom-left-radius: 4px;
            border-bottom-left-radius: 4px;
  }


2011/10/25

Colocación de elementos mediante CSS

La entrada de hoy está dedicada a problemas del día a día para la colocación de elementos mediante CSS con recetas sencillas para resolverlos.

En la prehistoria, si se quería un contenido centrado nada era más fácil que poner un <center></center>, pero eso, aparte de viejuno, ahora mismo como te pillen haciéndolo, te cortan las manos.
The <center> tag is supported in all major browsers. However, it is deprecated and should be avoided!
Así que ahora, los más listos habrán dicho que poniendo (con perdón del estyle inline) un <div style="text-align: center;"></div> ya se arregla todo y se quedan tan panchos. Pues sí y no. Si se quiere centrar un texto, va perfecto, pero... y si lo que se quiere centrar es otro contenedor como por ejemplo un div o un span... centraríamos el texto del segundo contenedor, pero no el contenedor en sí.

Pues para este tipo de disyuntivas, vamos a ver algunas recetas copy-paste.

Centrado de texto
Tal y como hemos visto, la propiedad CSS text-align:center hace el trabajo necesario.

Centrado de bloques de texto
Más que centrado, se le puede llamar márgenes equivalentes, que por ende, produce un efecto de centrado. Ni qué decir tiene, que sin especificar un ancho, no sirve de mucho.
P.bloqueCentradoDeTexto { margin-left: auto; margin-right: auto; width: 300px;}

Centrado de imágenes
Equivalente al centrado de texto. La única diferencia es que para que funcione, hay que convertir la imagen en un bloque con display:block.
img.imagenCentrada { display: block; margin-left: auto; margin-right: auto; width: 300px;}

Centrado vertical
Subamos la dificultad un nivel. En las tablas es muy sencillo... existe la propiedad CSS de vertical-align: middle, bueno, pues con otros elementos de bloque es también muy sencillo. El secreto es hacer que el contenedor de dichos elementos se comporte como una celda de tabla para aplicar la propiedad CSS de vertical-align dando además un alto al contendor.
div.containerDeBloqueCentradoVerticalmente { height: 500px; display: table-cell; vertical-align: middle;}

Final Boss. Colocando elementos en la esquina inferior derecha.
Hay dos maneras, o con un "float: right" que sirve en los casos en los que el contenedor crece con los contenidos, pero si tenemos un contenedor que no solo no crece si no que a lo mejor tiene un alto fijo, no funciona. Para ello, hay una receta sencilla para conseguirlo sin tener que recurrir a código javascript mediante position:absolute. La pega es para variar IE6, que tiene un bug calculando el alto de elementos, pero que se evitar mediante hacks. Para la receta, defino dos clases, una para el contenedor ".contenedor" y otra para el contenido ".alineadoEnLaEsquina" (en este caso un hipervínculo).
div.contenedor { 
    position: relative; 
    height: 1%; /* Hack para el IE6 */
}
a.alineadoEnLaEsquina { 
    position: absolute; 
    bottom: 0; /* en vez de 0 y 0 se puede poner otra cantidad */
    right: 0;  /* para simular el margen respecto a la esquina */
}

A disfrutarlo!.

2011/10/24

Quién me lee?

La verdad es que para ser éste un blog que nació únicamente como memoria digital de las cosillas que voy descubriendo por la web, viendo las estadíasticas me he dado cuenta de tres cosas muy curiosas...

La primera, es que mientras menos escribo más gente me lee... Cuando he estado publicando una o más entradas diarias no llegaba a 100 visitas al mes (y he contado a mi madre... dos veces). Sin embargo, ahora que me he tomado el verano de relax, en casi dos-tres meses sin escribir una línea, tengo unas 1500 visitas mensuales y creciendo exponencialmente. Curioso, no?

La segunda, es que los que me lean (si es que hay alguien) son personas muy tímidas. 0 comentarios en total. Al menos tengo que administrar pocos comentarios soeces y descalificaciones personales. Eso es de agradecer.

Lo tercero es la tecnología de los navegadores de la gente que me visita. Cuanto menos sorprendente. Os pego un extracto de las estadísticas:
Chrome29%
Google Desktop28%
Firefox24%
Internet Explorer10%
Safari5%
Opera1%
Mobile Safari<1%Hola Curro!
chromeframe<1%Todavía hay alguien (5) que lo usa! bien!
ThunderBrowse<1%Y éso qué es?
Mobile<1%1 visita. Ese soy yo :D

Es decir, que el navegador más usado por gente dedicada a aprender cosas sobre la web es el Chrome, que si lo juntamos con el Google Desktop, que supongo que es el Reader de Google, suma la friolera del 57% de las visitas provenientes de productos de Google!!!.

Luego viene el firefox y solamente un 10% de IE (no voy a hacer sangre desglosando por versiones :P)

En fin... si trabajara en los informativos de la tele, diría que los que usan Chrome son más listos, audaces y ávidos de conocimiento, pero como no es así, simplemente diré que son más y punto, por ahí no me pilláis, pillines!

Bueno, y esta cutre-entrada servirá para advertiros que la semana que viene postearé varios (>1) plugins de jQuery que he hecho. Os van a gustar. Estad atentos, y va por vosotros, esos casi 1500 ;)

2011/10/21

Copiando a Google, sí, ¿y?. Flexbox y overlay/dialog.

Retomemos viejas costumbres. Llevaba algún tiempo sin publicar nada básicamente porque llegaron las vacaciones y por pereza, sinceridad ante todo. Pero hay que luchar contra la apatía, así que para abrir boca, os voy a poner un cachito de código copiado vilmente desde Chrome aplicando las últimas tecnologías y ese toque minimalista de Google, pero al alcance de todos los navegadores (modernos). Concretamente, os hablo del combo overlay/caja de diálogo que se usa en la configuración de Chrome pero fácilmente aplicable a cualquier web. A continuación, una captura.
Las tecnologías principales que se han usado son el juego de propiedades CSS3 denominado flexbox que permite definir cajas de elementos distribuyendolas tanto a ellas como a sus contenidos dentro de la página y  varias propiedades visuales de CSS3 junto con sus variantes según el navegador, puestas en el orden correcto. De regalo, un clon de los botones de Chrome también, que se ven muy bonitos.

Para más información sobre las propiedades de flexbox podéis ver un magnífico artículo en HTMLRocks. Es tontería explicarlo de nuevo aquí porque es una página muy trabajada con muchos ejemplos y que hasta detecta si el navegador usado para verla es capaz de mostrar correctamente todos los ejemplos, así que no os pongáis a verla con el IE6, que os veo y luego me venís llorando!.

Para ver el código completo, en mi jsFiddle como es habitual.


El ejemplo se compone de dos partes. Por un lado una estructura muy sencilla en HTML
<!-- Overlay/dialog -->
<div id="overlay" class="overlay">
    <div id="myDialog" class="dialog">

        <!-- Título del diálogo -->
        <h1>Título del Diálogo</h1>

        <!-- Cuerpo del diálogo -->
        <div class="dialog-area">
            <p>CUERPO</p>
        </div

        <!-- Pie del diálogo -->
        <div class="dialog-footer">
            <div class="extensible">PIE con botones a la derecha sin float</div>
            <input type="button" value="Botón1"/>
            <input type="button" value="Botón2"/>
        </div>

    </div>
</div>

Y por el otro, un CSS algo más complejo (CSS3, por supuesto). Los comentarios además de ayudar a identificar los estilos, dan información sobre para qué y cómo se usan.

/* ================================================================================ */
/* Sample styles */
html {
    font-family: arial;
}

/* Sample dialog styles */
#myDialog {
    background-color: white;
    margin-bottom: 6px;
    margin-top: 6px;
    width: 500px;
}


/* ================================================================================ */
/* Overlay/Dialog styles */
.overlay {
    -webkit-transition: 0.25s opacity;
       -moz-transition: 0.25s opacity;
        -ms-transition: 0.25s opacity;
         -o-transition: 0.25s opacity;
            transition: 0.25s opacity;  

    background: -webkit-radial-gradient(rgba(127, 127, 127, 0.5), rgba(127, 127, 127, 0.5) 35%, rgba(0, 0, 0, 0.7));
    background:    -moz-radial-gradient(rgba(127, 127, 127, 0.5), rgba(127, 127, 127, 0.5) 35%, rgba(0, 0, 0, 0.7));
    background:      -o-radial-gradient(rgba(127, 127, 127, 0.5), rgba(127, 127, 127, 0.5) 35%, rgba(0, 0, 0, 0.7));
    background:     -ms-radial-gradient(rgba(127, 127, 127, 0.5), rgba(127, 127, 127, 0.5) 35%, rgba(0, 0, 0, 0.7));
    background:         radial-gradient(rgba(127, 127, 127, 0.5), rgba(127, 127, 127, 0.5) 35%, rgba(0, 0, 0, 0.7));
    
    position: fixed;
    left: 0; top: 0; right: 0; bottom: 0;
    padding: 20px; padding-bottom: 130px;
    z-index: 10;
}


.overlay .dialog {
    -webkit-user-select: none;
       -moz-user-select: none;

    -webkit-box-shadow: 0px 5px 80px #505050;
       -moz-box-shadow: 0px 5px 80px #505050;
         -o-box-shadow: 0px 5px 80px #505050;
        -ms-box-shadow: 0px 5px 80px #505050;
            box-shadow: 0px 5px 80px #505050;

    background: white;
    border: 1px solid #BCC1D0;

    -webkit-border-radius: 2px;
       -moz-border-radius: 2px;
     -khtml-border-radius: 2px;
            border-radius: 2px;

    position: relative;
    min-width: 400px;
    padding: 0;
}


/* Dialog heading */
.overlay .dialog h1 {
    -webkit-padding-end: 24px; /* Specifies the space between an element's left or right border and its contents, depending on the writing direction */
       -moz-padding-end: 24px;

    -webkit-user-select: none;
       -moz-user-select: none;

    background: -webkit-linear-gradient(white, #F8F8F8);
    background:    -moz-linear-gradient(white, #F8F8F8);
    background:      -o-linear-gradient(white, #F8F8F8);
    background:         linear-gradient(white, #F8F8F8);

    border-bottom: 1px solid rgba(188, 193, 208, .5);
    padding: 10px 15px 8px 15px;
    margin: 0;

    font-size: 105%;
    font-weight: bold;
    color: #53637D;
    text-shadow: white 0 1px 2px;
}


/* Dialog area */
.overlay .dialog .dialog-area {
    -webkit-user-select: none;
       -moz-user-select: none;

    padding: 10px 15px;
}


/* Dialog footer */
.overlay .dialog .dialog-footer {

    border-top: 1px solid rgba(188, 193, 208, .5);
    padding: 12px;
}


/* ================================================================================ */
/* Flexible box model */
/* http://www.html5rocks.com/en/tutorials/flexbox/quick/ */
.overlay {
    display: -webkit-box;            /* Enables flexible box model */
    -webkit-box-orient: horizontal; /* How should the box's children be aligned */
      -webkit-box-pack: center;    /* Sets the alignment of the box along the box-orient axis. . So if box-orient is horizontal it will chose how the box's children are aligned horizontally, and vice-versa. */
     -webkit-box-align: center;   /* Basically box-pack's brother property. Sets how the box's children are aligned in the box. If the orientation is horizontal it will decide the alignment vertically and vice-versa */

    display: -moz-box;
    -moz-box-orient: horizontal;
      -moz-box-pack: center;
     -moz-box-align: center;

    display: box;
    box-orient: horizontal;
      box-pack: center;
     box-align: center;
}

.overlay .dialog .dialog-footer {
    display: -webkit-box;
    -webkit-box-orient: horizontal;
      -webkit-box-pack: end;
     -webkit-box-align: center;

    display: -moz-box;
    -moz-box-orient: horizontal;
      -moz-box-pack: end;
     -moz-box-align: center;

    display: box;
    box-orient: horizontal;
      box-pack: end;
     box-align: center;
}

.extensible {
    -webkit-box-flex: 1;  /* Float value. Extensible up to 1 (or value) times its width (or height depending on parent's orientation) */
       -moz-box-flex: 1;
            box-flex: 1;
}


/* ================================================================================ */
/* Chrome buttons */
button, input[type="button"], input[type="submit"] {
    -webkit-border-radius: 2px;
       -moz-border-radius: 2px;
     -khtml-border-radius: 2px;
            border-radius: 2px;

    -webkit-box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.1);

    -webkit-user-select: none;
       -moz-user-select: none;

    background: -webkit-linear-gradient(#FAFAFA, #F4F4F4 40%, #E5E5E5);
    background:    -moz-linear-gradient(#FAFAFA, #F4F4F4 40%, #E5E5E5);
    background:      -o-linear-gradient(#FAFAFA, #F4F4F4 40%, #E5E5E5);
    background:         linear-gradient(#FAFAFA, #F4F4F4 40%, #E5E5E5);

    border: 1px solid #AAA;
    color: #444;
    font-size: inherit;
    margin-bottom: 0px;
    min-width: 4em;
    padding: 3px 12px 3px 12px;
}

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.