Ticker para feeds RSS en JQuery

Hace poco me puse a trastear con JQuery, que aún no conocía, y opté por practicar desarrollando un widget que me permitiera leer feeds rss y mostrarlos en formato ticker horizontal. Me fui entusiasmando con el asunto y terminé creando una pequeña aplicación para mostrar varios tickers reordenables distintos organizados por categorías en carpetas verticales con opciones para añadir, renombrar, eliminar y guardar. En este articulo sólo hablaré del widget básico, si alguien quiere descargarse la aplicación completa puede encontrarla en GitHub.


Para empezar necesitamos:

  • JQuery 1.11.0 y 2.1.0 (para soporte IE9 o superior), instalamos ambos en js/jquery
  • JQuery UI 1.10.4, instalamos en js/jquery.ui
  • Dinamyc Web Scroller, instalamos en js/dw.scroller
  • JQuery UI themes, instalamos en css/ui-themes, el tema usado en la demo es black-tie

Dynamic Web Scroller (DWS) es la librería que suelo usar cuando necesito desplazamientos de información en pantalla, creo que con JQuery UI también se dispone de dicha funcionalidad pero no lo comprobé. El widget es muy simple: se le envía la url del feed (options.url) y una función callback si es preciso. La razón de dicho callback es que el feed se carga en modo asíncrono (usando la Google Feed API). El widget invoca al API pasándole una función callback interna (_loadPosts()) la cual, después de generar el ticker con su contenido en pantalla, termina llamando al callback del usuario. Asi podemos enlazar distintos tickers si lo deseamos como se ve en la demo.
Para crear el ticker se genera un árbol DOM adaptado a los requerimientos de la librería DWS:
<div class="container">
     <div class="origin">
          <a href="enlace a la web del feed">
          <img src="favicon de la web">
          <span>título del feed</span>
          </a>
     </div>
     <div class="capsule">
          <div class="nest">
                <div class="ticker">
                      <span class="news-header">título y resumen de cada noticia</span>
                      [...]
                </div>
          </div>
     </div>
</div>
Una vez generado se recogen los valores ID de los divs capsule, nest, y news-header en DWS para que inicie el scroll. El div origin se usa para contener el título y el favicon de la web. Si inspeccionamos el div ticker una vez en marcha comprobaremos que por cada titular se ha generado un news-header y que estos se repiten dos o mas veces a partir de un span determinado por un ID de tipo nt-news-header-idxxxxxxxx. Es decir, la lista de titulares se genera por duplicado, triplicado o más. Ello se debe a la forma en que funciona DWS: para trabajar correctamente necesita que la lista de titulares a mostrar ocupe por lo menos 2 veces el ancho de la zona visible para el usuario, por lo tanto el widget se encarga de comprobar las medidas de cada titular que se inserta y decide posteriormente cuantas copias más debe añadir (en _procesaEntradas() y _processFeed()). Los nombres de estas clases y otros valores y funciones comunes se encuentran en el archivo js/commons.js.

(function($)
{
 var _self;
 var _base;
 var _cnt = 1;
 var _titleticker;
 var _me;
 var _refresh_callback;
 var _max_feed_loads = 100;
  
 $.widget('news.ticker', {
  
  options: 
  {
   url: null,
   callback: $.noop
  },
  
  _init: function()
  {
   _self = this;
   if (_self.options.url !== null)
   {   
    _base = _self.element;
    _self._idx = genKey();
    _self._selector = idNews.id().container + _self._idx;
    _self._callback = function (TotalFeed) { _self._loadPosts(TotalFeed); }
    _self._loadFeed(_self.options.url);
   }
  },
  
  _setOption: function(name, value)
  {
      $.Widget.prototype._setOption.apply(this, arguments);   
   switch (name)
   { // Si cambia la url (parametro 1) callback = null, si existe callback se inicializa en siguiente llamada
    // Esta operación evita llamadas recursivas al mismo callback
    case 'url': this.options.callback = $.noop;
       break;
   }
  },
  
  // Se crean los divs container, origin, capusle, nest y ticker
  
  _createTicker: function()
  {
   $(_base).append(
    $('<div/>', {
     class: classNews.cssclass().container + ' ' + uiClass.ui().container,
     id: _self._selector,
    }).append(
     $('<div/>', {
      class: classNews.cssclass().origin + ' ' + uiClass.ui().origin,
      id: idNews.id().origin + _self._idx,
     }),
     $('<div/>', {
      class: classNews.cssclass().capsule  + ' ' + uiClass.ui().capsule,
      id: idNews.id().capsule + _self._idx,
     }).append($('<div/>', {
       class: classNews.cssclass().nest,
       id: idNews.id().nest + _self._idx,
      }).append($('<div/>', {
        class: classNews.cssclass().ticker,
        id: idNews.id().ticker + _self._idx,
       })
      )    
     )
    )
   );
   $(_base).tooltip({ hide: {duration: 1000 }});
  },
  
  // Llamada al API de Google para cargar el feed
  
  _loadFeed: function(url)
  {
   var feed = new google.feeds.Feed(url);
   feed.setNumEntries(_max_feed_loads);
   feed.load(function(TotalFeed) { _self._callback(TotalFeed); });   
  },
  
  // Se genera el ticker si existen feeds y se llama al callback del usuario (si existe)
  
  _loadPosts: function(TotalFeed)
  {
   if (TotalFeed.feed.entries.length > 0)
   {
    _self._createTicker();
    _self._processFeed(TotalFeed); 
   }
   _self._trigger('callback');
  },
  
  // Se genera cada span news-header para titular y resumen. Si repeat == true se marca el span como inicio de la copia de la lista de titulares
  
  _procesaEntradas: function(TotalFeed, iframe, favicon, idxcode, repeat)
  {
   var cancelRepeat = function() { repeat = false; return idNews.id().header + idxcode; };
   var spanwidth = 0;
   
   $.each(TotalFeed.feed.entries, function (idx, val) {
    var href = $('<a/>', {
       id: commons.label().urltitle + idxcode + '-' + _cnt,
       href: val.link,
       text: decodeEntities(val.title),
       target: '_blank'
      });
    $(iframe).append($('<span/>', {
      class: classNews.cssclass().header,
      id: ((repeat) ? cancelRepeat() : commons.label().spantitle + idxcode + '-' +  _cnt),
     }).append(
      $('<img/>', {
       class: classNews.cssclass().favicon_thumb,
       src: favicon
      }),     
      $(href)
     ));
//    $('#url' + _idx + idx).attr('title', decodeEntities(val.content)); // Sin decode aparece el formato HTML, con decode solo textos
    $('#' + commons.label().urltitle + idxcode + '-' + _cnt).attr('title', val.content);
    spanwidth += $('#' + commons.label().spantitle + idxcode + '-' + _cnt++).width();
   }); 
   return spanwidth;  
  },
  
  // Se genera el título y favicon del feed y se lla a _procesaEntradas() para crear la lista de titulares
  
  _processFeed: function(TotalFeed)
  {
   _self._selector = '#' + _self._selector;
   iframe = $(_self._selector).find('#' + idNews.id().ticker + _self._idx);
   var favicon = TotalFeed.feed.link.replace(/(:\/\/[^\/]+).*$/, '$1') + '/favicon.ico';
   
   _titleticker = decodeEntities(TotalFeed.feed.title);
   $(_self._selector).children('#' + idNews.id().origin + _self._idx).append(
    $('<a/>', {
     href: TotalFeed.feed.link,
     target: '_blank'
    }).append(
     $('<img/>', {
      class: classNews.cssclass().favicon,
      src: favicon
     }),
     $('<span/>', {
      text: decodeEntities(TotalFeed.feed.title)
     }).css('position', 'absolute').css('float', 'left')
    )
   )   
   var wd = _self._procesaEntradas(TotalFeed, iframe, favicon, _self._idx, false); 
   for (i = 0; i < ~~(($('.' + classNews.cssclass().container).width() * 2) / wd) + 1; i++)
    _self._procesaEntradas(TotalFeed, iframe, favicon, _self._idx, (i == 0));
   _self._activaScroll();
  },

  _activaScroll: function()
  {
   if (DYN_WEB.Scroll_Div.isSupported())
   {
    DYN_WEB.Event.domReady( function()
    {
     var wndo2 = new DYN_WEB.Scroll_Div(idNews.id().capsule + _self._idx, idNews.id().nest + _self._idx);
     wndo2.makeSmoothAuto( {axis: 'h', bRepeat: true, repeatId: idNews.id().header + _self._idx, speed: 60, bPauseResume: true} );
    });
   }
  },
  
  // Estas funciones se usan en jTicker
  
  idTicker: function()
  {
   return _self._selector;
  },
  
  titleTicker: function()
  {
   return _titleticker;
  },
  
  // Funciones para refrescar el contenido del ticker
  
  _feedRefresh: function(TotalFeed)
  {
   var iframe = $(_me).find('.' + classNews.cssclass().ticker);
   var favicon = $(_me).children('.' + classNews.cssclass().origin).children('a').children('img').attr('src');
   var idx = $(iframe).attr('id').substr(idNews.id().ticker.length);
   var wd = _self._procesaEntradas(TotalFeed, iframe, favicon, idx, false); 
   for (i = 0; i < ~~(($('.' + classNews.cssclass().container).width() * 2) / wd) + 1; i++)
    _self._procesaEntradas(TotalFeed, iframe, favicon, idx, (i == 0));
//   _self._activaScroll(); // No hace falta reactivar scroll, sigue activo y se usan los mismos id's
   _refresh_callback();
  },
  
  feedRefresh: function(callback)
  {
   _me = this.element;
   _refresh_callback = callback;
   $(_me).find('.' + classNews.cssclass().ticker).children('.news-header').remove();
   _self._callback = function(TotalFeed) { _self._feedRefresh(TotalFeed); };
   _self._loadFeed($(_me).attr('feedurl'));
  }
  
 });
}(jQuery));

 Para probarlo creamos un index.html de la forma:


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>jTicker</title>
<link id="css1" rel="stylesheet" href="css/ui-themes/black-tie/jquery-ui.css">
<link id="css2" rel="stylesheet" href="css/ui-themes/black-tie/jquery.ui.theme.css">
<link id="css3" rel="stylesheet" type="text/css" href="css/jquery.news.ticker.css"/>
<link id="css3" rel="stylesheet" type="text/css" href="css/jquery.news.override.css"/>
<script src="http://www.google.com/jsapi" type="text/javascript"></script>
<!--[if lt IE 9]>
<script src="js/jquery/jquery-1.11.0.min.js" type="text/javascript"></script>
<![endif]-->
<!--[if gte IE 9]><!-->
<script src="js/jquery/jquery-2.1.0.min.js" type="text/javascript"></script>
<!--<![endif]-->
<script src="js/jquery.ui/jquery-ui-1.10.4.custom.min.js" type="text/javascript"></script>
<script src="js/dw.scroller/dw_con_scroller.js" type="text/javascript"></script>
<script src="js/jquery.news/jquery.news.commons.js" type="text/javascript"></script>
<script src="js/jquery.news/jquery.news.ui.tooltip.js" type="text/javascript"></script>
<script src="js/jquery.news/jquery.news.ticker.js" type="text/javascript"></script>
<style>
#test {
 width: 1000px;
 margin-left:auto;
 margin-right: auto;
}
</style>
</head>
<body> 
<div id="test"></div>
</body>
<script type="text/javascript">

 function tickers()
 {
  var test = '#test';

  $(test).ticker({url: 'http://codementia.blogspot.com/feeds/posts/default',
    callback: function() { $(test).ticker({url: 'http://www.microsiervos.com/rss-ciencia.xml',
             callback: function() { $(test).ticker({url: 'http://exampleusername.livejournal.com/data/atom' 
               })}
         })}
     });
  $(test).sortable();
 }
 google.load("feeds", "1");
 google.setOnLoadCallback( function() { tickers(); });

</script>
</html>


Con lo que obtendremos un grupo de tickers apilados:

Al usar la API de Google sólo podremos leer feeds en formato RSS y ATOM. El widget adicional que se necesita (news.ui.tooltip) simplemente reescribe el método de jQuery UI para la propiedad title de un div, que es donde se almacena el resumen de cada titular. Se puede descargar el fuente aquí.

2 comentarios: