Mostrando entradas con la etiqueta jQuery. Mostrar todas las entradas
Mostrando entradas con la etiqueta jQuery. Mostrar todas las entradas

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/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?

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/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  */